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,966 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+ sys.path.insert(0, HERE)
13
+ from image_plan_lint import lint # noqa: E402
14
+ from image_prompt_rewrite import rewrite_plan # noqa: E402
15
+
16
+
17
+ class ImagePromptRewriteTest(unittest.TestCase):
18
+ def test_rewrites_generic_text_to_image_with_page_anchors(self):
19
+ with tempfile.TemporaryDirectory() as d:
20
+ build = Path(d)
21
+ (build / "user_request.md").write_text("用知乎长文生成能源电网智能巡检平台 PPT", encoding="utf-8")
22
+ (build / "design.json").write_text(
23
+ json.dumps(
24
+ {
25
+ "pages": [
26
+ {
27
+ "page": 11,
28
+ "title": "多终端协同巡检",
29
+ "one_sentence_takeaway": "调度端、移动端和现场设备共享同一张电网运行图。",
30
+ "page_grammar": "product-closeup",
31
+ "visual_focus": "电网巡检终端协同查看运行态势",
32
+ "safe_text_zone": "left",
33
+ }
34
+ ]
35
+ },
36
+ ensure_ascii=False,
37
+ ),
38
+ encoding="utf-8",
39
+ )
40
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
41
+ (build / "img_plan.json").write_text(
42
+ json.dumps(
43
+ [
44
+ {
45
+ "file": "ai_p11.png",
46
+ "mode": "text-to-image",
47
+ "aspect": "16:9",
48
+ "prompt": "professional product photography of a sleek device on a dark background",
49
+ }
50
+ ],
51
+ ensure_ascii=False,
52
+ ),
53
+ encoding="utf-8",
54
+ )
55
+ report = rewrite_plan(build)
56
+ self.assertEqual(report["changed_count"], 1)
57
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
58
+ prompt = plan[0]["prompt"]
59
+ self.assertIn("多终端协同巡检", prompt)
60
+ self.assertIn("电网", prompt)
61
+ self.assertNotIn("aviation", prompt.lower())
62
+ self.assertIn("no game controller", prompt)
63
+ self.assertIn("no cosmetic bottle", prompt)
64
+
65
+ def test_rewrites_i2i_with_page_takeaway_and_preserve_policy(self):
66
+ with tempfile.TemporaryDirectory() as d:
67
+ build = Path(d)
68
+ (build / "user_request.md").write_text("客户图必须图生图增强后使用", encoding="utf-8")
69
+ (build / "design.json").write_text(
70
+ json.dumps(
71
+ {
72
+ "pages": [
73
+ {
74
+ "page": 5,
75
+ "title": "采集终端",
76
+ "one_sentence_takeaway": "多种采集终端把现场状态变成可追溯记录。",
77
+ "safe_text_zone": "right",
78
+ "visual": {"source_file": "src_1.png"},
79
+ }
80
+ ]
81
+ },
82
+ ensure_ascii=False,
83
+ ),
84
+ encoding="utf-8",
85
+ )
86
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
87
+ (build / "img_plan.json").write_text(
88
+ json.dumps(
89
+ [
90
+ {
91
+ "file": "enhanced_src_1.png",
92
+ "mode": "image-to-image",
93
+ "source_file": "src_1.png",
94
+ "aspect": "16:9",
95
+ "enhance_style": "cutout",
96
+ "prompt": "restore quality",
97
+ }
98
+ ],
99
+ ensure_ascii=False,
100
+ ),
101
+ encoding="utf-8",
102
+ )
103
+ rewrite_plan(build)
104
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
105
+ prompt = plan[0]["prompt"]
106
+ self.assertIn("Use the attached image", prompt)
107
+ # 句子切片「多种采集终端把现场状态变」被碎片过滤剔除,名词锚「采集终端」保留页面绑定。
108
+ self.assertIn("采集终端", prompt)
109
+ self.assertIn("transparent", prompt)
110
+ # 2026-07-03 起:i2i 不再拼 Slide title/Core message 原句(增强图常当 hero/背景层,
111
+ # 原句会被画成新文字);尾部负面从 no text 改为 no new text(dense 忠实还原不删原文字)。
112
+ self.assertIn("no new text", prompt)
113
+ self.assertNotIn("Slide title:", prompt)
114
+ self.assertNotIn("Core message:", prompt)
115
+
116
+ def _i2i_setup(self, build, enhance_style, source_file, source_plan_entry=None, takeaway="", prompt="old prompt"):
117
+ (build / "user_request.md").write_text("测试", encoding="utf-8")
118
+ (build / "design.json").write_text(
119
+ json.dumps({"pages": [{"page": 1, "title": "测试页", "one_sentence_takeaway": takeaway,
120
+ "safe_text_zone": "right", "visual": {"source_file": source_file}}]},
121
+ ensure_ascii=False),
122
+ encoding="utf-8",
123
+ )
124
+ (build / "source_layout_plan.json").write_text(
125
+ json.dumps({"images": [source_plan_entry] if source_plan_entry else []}, ensure_ascii=False),
126
+ encoding="utf-8",
127
+ )
128
+ enhanced = f"enhanced_{source_file}"
129
+ (build / "img_plan.json").write_text(
130
+ json.dumps([{"file": enhanced, "mode": "image-to-image", "source_file": source_file,
131
+ "aspect": "16:9", "enhance_style": enhance_style, "prompt": prompt}],
132
+ ensure_ascii=False),
133
+ encoding="utf-8",
134
+ )
135
+ rewrite_plan(build)
136
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
137
+ return plan[0]["prompt"]
138
+
139
+ def test_dense_with_embedded_text_strips_caption_preserves_structure_regression(self):
140
+ # 2026-07-03 真机 src_6 回归事故:_rewrite_i2i 此前无条件重生保真 prompt,把
141
+ # plan_source_layout.py 已算好的去字提示词(text_policy=remove_all_text)覆盖掉——
142
+ # 原图标题「AI全景相机优势」因此没被去掉。现在必须读 source_plan 引用实际文字。
143
+ with tempfile.TemporaryDirectory() as d:
144
+ build = Path(d)
145
+ entry = {
146
+ "file": "src_6.png", "text_policy": "remove_all_text",
147
+ "embedded_text": [
148
+ {"text": "AI全景相机优势", "region": "顶部左侧标题"},
149
+ {"text": "多台监控", "region": "上排中间蓝色箭头内"},
150
+ ],
151
+ }
152
+ prompt = self._i2i_setup(build, "dense", "src_6.png", entry)
153
+ self.assertIn("AI全景相机优势", prompt)
154
+ self.assertIn("多台监控", prompt)
155
+ self.assertIn("REMOVED", prompt)
156
+ self.assertIn("layout, sub-images, comparison structure", prompt)
157
+ self.assertNotIn("do not rewrite charts, screenshots, numbers, labels, UI structure or data", prompt)
158
+
159
+ def test_dense_without_embedded_text_keeps_faithful_preserve_regression(self):
160
+ # 回归锁:chart 类图表(text_policy=preserve_data_text 或无 embedded_text)不受影响,
161
+ # 仍走原有的无条件保真措辞——图表坐标轴/图例数值是数据本身,不能被误去字。
162
+ with tempfile.TemporaryDirectory() as d:
163
+ build = Path(d)
164
+ entry = {"file": "chart_1.png", "text_policy": "preserve_data_text", "embedded_text": []}
165
+ prompt = self._i2i_setup(build, "dense", "chart_1.png", entry)
166
+ self.assertIn("do not rewrite charts, screenshots, numbers, labels, UI structure or data", prompt)
167
+ self.assertNotIn("REMOVED", prompt)
168
+
169
+ def test_dense_missing_source_plan_entry_keeps_faithful_preserve(self):
170
+ # source_layout_plan.json 里找不到对应条目(旧产物/未透传)→ 静默退回原有保真措辞,不报错。
171
+ with tempfile.TemporaryDirectory() as d:
172
+ build = Path(d)
173
+ prompt = self._i2i_setup(build, "dense", "src_9.png", None)
174
+ self.assertIn("do not rewrite charts, screenshots, numbers, labels, UI structure or data", prompt)
175
+
176
+ def test_cutout_with_embedded_text_appends_removal_instruction(self):
177
+ with tempfile.TemporaryDirectory() as d:
178
+ build = Path(d)
179
+ entry = {"file": "src_7.png", "text_policy": "remove_all_text",
180
+ "embedded_text": [{"text": "720Avatar", "region": "车身侧面"}]}
181
+ prompt = self._i2i_setup(build, "cutout", "src_7.png", entry)
182
+ self.assertIn("transparent", prompt)
183
+ self.assertIn("720Avatar", prompt)
184
+ self.assertIn("completely removed", prompt)
185
+
186
+ def test_generic_bg_hero_with_embedded_text_appends_removal_instruction(self):
187
+ with tempfile.TemporaryDirectory() as d:
188
+ build = Path(d)
189
+ entry = {"file": "src_8.png", "text_policy": "remove_all_text",
190
+ "embedded_text": [{"text": "PILOT LOOK", "region": "海报角标"}]}
191
+ prompt = self._i2i_setup(build, "hero", "src_8.png", entry)
192
+ self.assertIn("clean negative space for editable PPT text", prompt)
193
+ self.assertIn("PILOT LOOK", prompt)
194
+ self.assertIn("completely removed", prompt)
195
+
196
+ def test_i2i_tier1_keeps_genuine_prompt_with_complete_deltext_clause_unchanged(self):
197
+ # Tier 1:original 已经像真 i2i 指令、且完整引用了所有 embedded_text → 原样保留,
198
+ # 一个字都不改(包括只有页面定制才会有的独特措辞),不走模板重建。
199
+ with tempfile.TemporaryDirectory() as d:
200
+ build = Path(d)
201
+ entry = {
202
+ "file": "src_6.png", "text_policy": "remove_all_text",
203
+ "embedded_text": [
204
+ {"text": "AI全景相机优势", "region": "顶部左侧标题"},
205
+ {"text": "多台监控", "region": "上排中间蓝色箭头内"},
206
+ ],
207
+ }
208
+ custom_prompt = (
209
+ "Use the attached image as the strict visual reference. Preserve the real main subject. "
210
+ "Faithfully keep the exact same layout and comparison structure, misty harbor at dusk mood. "
211
+ "However, the original image also has this exact baked-in text that must be completely "
212
+ "REMOVED, leaving clean empty space in its place: 'AI全景相机优势' (顶部左侧标题), "
213
+ "'多台监控' (上排中间蓝色箭头内). Never reproduce or invent any text."
214
+ )
215
+ prompt = self._i2i_setup(build, "dense", "src_6.png", entry, prompt=custom_prompt)
216
+ self.assertEqual(prompt, custom_prompt)
217
+
218
+ def test_i2i_tier1_keeps_asis_when_no_deltext_required_and_no_source_plan_entry(self):
219
+ # Tier 1:genuine i2i 指令但 source_layout_plan.json 没有对应条目 → 仍然原样保留,
220
+ # 锁定"无 plan 条目 ≠ 强制重建"这个刻意设计(多数 i2i 条目本来就没有去字要求)。
221
+ with tempfile.TemporaryDirectory() as d:
222
+ build = Path(d)
223
+ custom_prompt = (
224
+ "Use the attached image as the strict visual reference. Preserve the real main subject "
225
+ "and key factual content. Faithfully restore clarity only; do not rewrite charts, data. "
226
+ "golden hour rooftop scene."
227
+ )
228
+ prompt = self._i2i_setup(build, "dense", "src_9.png", None, prompt=custom_prompt)
229
+ self.assertEqual(prompt, custom_prompt)
230
+
231
+ def test_i2i_tier2_patches_missing_deltext_preserves_custom_wording_src6_regression(self):
232
+ # Tier 2 端到端复刻真机 src_6 事故:设计 LLM 抄来的 prompt 本身没问题(有 i2i 指令特征 +
233
+ # 独特页面措辞),只是缺去字指令 → 必须打补丁保留原有措辞,而不是被推翻重建成写死模板。
234
+ with tempfile.TemporaryDirectory() as d:
235
+ build = Path(d)
236
+ entry = {
237
+ "file": "src_6.png", "text_policy": "remove_all_text",
238
+ "embedded_text": [
239
+ {"text": "AI全景相机优势", "region": "顶部左侧标题"},
240
+ {"text": "多台监控", "region": "上排中间蓝色箭头内"},
241
+ {"text": "1台全景", "region": "下方对比标签"},
242
+ ],
243
+ }
244
+ custom_prompt = (
245
+ "Use the attached image as the strict visual reference. Preserve the real main subject "
246
+ "and key factual content. Faithfully keep the exact same layout, sub-images, comparison "
247
+ "structure and every non-text visual detail unchanged, cinematic aviation hangar mood."
248
+ )
249
+ prompt = self._i2i_setup(build, "dense", "src_6.png", entry, prompt=custom_prompt)
250
+ self.assertIn("cinematic aviation hangar mood", prompt)
251
+ self.assertIn("AI全景相机优势", prompt)
252
+ self.assertIn("多台监控", prompt)
253
+ self.assertIn("1台全景", prompt)
254
+ self.assertIn("REMOVED", prompt)
255
+
256
+ def test_i2i_tier2_patch_partial_embedded_text_quoted_still_patches(self):
257
+ # 原 prompt 只引用了 embedded_text 中的一条 → 仍判定需要 Tier 2 补丁(追加完整引用集)。
258
+ with tempfile.TemporaryDirectory() as d:
259
+ build = Path(d)
260
+ entry = {
261
+ "file": "src_6.png", "text_policy": "remove_all_text",
262
+ "embedded_text": [
263
+ {"text": "AI全景相机优势", "region": "顶部左侧标题"},
264
+ {"text": "多台监控", "region": "上排中间蓝色箭头内"},
265
+ ],
266
+ }
267
+ custom_prompt = (
268
+ "Use the attached image as the strict visual reference. Preserve the real main subject. "
269
+ "The original has this exact baked-in text that must be completely removed: 'AI全景相机优势'."
270
+ )
271
+ prompt = self._i2i_setup(build, "dense", "src_6.png", entry, prompt=custom_prompt)
272
+ self.assertIn("多台监控", prompt)
273
+
274
+ def test_i2i_tier2_patch_is_idempotent_second_run(self):
275
+ # 打补丁后再跑一次必须幂等:changed_count==0,img_plan.json 逐字节不变。
276
+ with tempfile.TemporaryDirectory() as d:
277
+ build = Path(d)
278
+ entry = {
279
+ "file": "src_6.png", "text_policy": "remove_all_text",
280
+ "embedded_text": [{"text": "AI全景相机优势", "region": "顶部左侧标题"}],
281
+ }
282
+ custom_prompt = (
283
+ "Use the attached image as the strict visual reference. Preserve the real main subject "
284
+ "and key factual content. Faithfully keep the exact same layout unchanged, dusk mood."
285
+ )
286
+ self._i2i_setup(build, "dense", "src_6.png", entry, prompt=custom_prompt)
287
+ plan_after_first = (build / "img_plan.json").read_text(encoding="utf-8")
288
+ second = rewrite_plan(build)
289
+ self.assertEqual(second["changed_count"], 0, second)
290
+ self.assertEqual((build / "img_plan.json").read_text(encoding="utf-8"), plan_after_first)
291
+
292
+ def test_i2i_tier3_fallback_when_prompt_missing_i2i_signature(self):
293
+ # 原 prompt 是纯 t2i 风格文本误放在 i2i 槽位(不含任何 i2i 特征词)→ 走 Tier 3 全量重建。
294
+ with tempfile.TemporaryDirectory() as d:
295
+ build = Path(d)
296
+ entry = {"file": "src_6.png", "text_policy": "remove_all_text",
297
+ "embedded_text": [{"text": "AI全景相机优势", "region": "顶部左侧标题"}]}
298
+ t2i_style_prompt = "professional product photography of a sleek device on a dark background"
299
+ prompt = self._i2i_setup(build, "dense", "src_6.png", entry, prompt=t2i_style_prompt)
300
+ self.assertIn("AI全景相机优势", prompt)
301
+ self.assertIn("REMOVED", prompt)
302
+ self.assertIn("layout, sub-images, comparison structure", prompt)
303
+ self.assertNotIn("sleek device", prompt)
304
+
305
+ def test_i2i_tier_counts_reported_in_summary(self):
306
+ # report 里的 i2i_tier_counts 应准确反映每条 i2i 走了哪一层。
307
+ with tempfile.TemporaryDirectory() as d:
308
+ build = Path(d)
309
+ (build / "user_request.md").write_text("测试", encoding="utf-8")
310
+ (build / "design.json").write_text(json.dumps({"pages": []}, ensure_ascii=False), encoding="utf-8")
311
+ (build / "source_layout_plan.json").write_text(
312
+ json.dumps({"images": [
313
+ {"file": "src_1.png", "text_policy": "remove_all_text",
314
+ "embedded_text": [{"text": "标题A", "region": "顶部"}]},
315
+ {"file": "src_2.png", "text_policy": "remove_all_text",
316
+ "embedded_text": [{"text": "标题B", "region": "顶部"}]},
317
+ ]}, ensure_ascii=False),
318
+ encoding="utf-8",
319
+ )
320
+ kept_prompt = (
321
+ "Use the attached image as the strict visual reference. Preserve the real main subject. "
322
+ "The original has this exact baked-in text that must be completely removed: '标题A'."
323
+ )
324
+ patch_prompt = (
325
+ "Use the attached image as the strict visual reference. Preserve the real main subject "
326
+ "and key factual content. Faithfully keep the exact same layout unchanged."
327
+ )
328
+ (build / "img_plan.json").write_text(
329
+ json.dumps([
330
+ {"file": "enhanced_src_1.png", "mode": "image-to-image", "source_file": "src_1.png",
331
+ "enhance_style": "dense", "prompt": kept_prompt},
332
+ {"file": "enhanced_src_2.png", "mode": "image-to-image", "source_file": "src_2.png",
333
+ "enhance_style": "dense", "prompt": patch_prompt},
334
+ {"file": "ai_p1.png", "mode": "text-to-image", "prompt": ""},
335
+ ], ensure_ascii=False),
336
+ encoding="utf-8",
337
+ )
338
+ report = rewrite_plan(build)
339
+ self.assertEqual(report["i2i_tier_counts"]["kept_as_is"], 1, report)
340
+ self.assertEqual(report["i2i_tier_counts"]["patched_missing_deltext"], 1, report)
341
+
342
+ def test_background_prompt_does_not_preserve_chapter_numbers(self):
343
+ with tempfile.TemporaryDirectory() as d:
344
+ build = Path(d)
345
+ (build / "user_request.md").write_text("做产品详情页式 PPT", encoding="utf-8")
346
+ (build / "design.json").write_text(
347
+ json.dumps(
348
+ {
349
+ "pages": [
350
+ {
351
+ "page": 2,
352
+ "title": "四段导览",
353
+ "one_sentence_takeaway": "先建立全文阅读地图。",
354
+ "page_grammar": "cinematic-feature",
355
+ "visual_focus": "4个章节序号浮在浅蓝矢量准线上",
356
+ "safe_text_zone": "left",
357
+ }
358
+ ]
359
+ },
360
+ ensure_ascii=False,
361
+ ),
362
+ encoding="utf-8",
363
+ )
364
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
365
+ (build / "img_plan.json").write_text(
366
+ json.dumps(
367
+ [
368
+ {
369
+ "file": "ai_p2.png",
370
+ "mode": "text-to-image",
371
+ "aspect": "16:9",
372
+ "prompt": "light blue route background with chapter numbers 1 2 3 4",
373
+ }
374
+ ],
375
+ ensure_ascii=False,
376
+ ),
377
+ encoding="utf-8",
378
+ )
379
+ report = rewrite_plan(build)
380
+ self.assertEqual(report["changed_count"], 1)
381
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
382
+ prompt = plan[0]["prompt"]
383
+ self.assertNotIn("chapter numbers", prompt.lower())
384
+ self.assertNotIn("1 2 3 4", prompt)
385
+ self.assertIn("complementary background", prompt)
386
+ self.assertIn("no text", prompt)
387
+
388
+ def test_background_prompt_does_not_copy_foreground_layout(self):
389
+ with tempfile.TemporaryDirectory() as d:
390
+ build = Path(d)
391
+ (build / "user_request.md").write_text("做产品详情页式 PPT", encoding="utf-8")
392
+ (build / "design.json").write_text(
393
+ json.dumps(
394
+ {
395
+ "pages": [
396
+ {
397
+ "page": 5,
398
+ "title": "证据形式变了",
399
+ "one_sentence_takeaway": "多台监控拍出分屏碎片,一台全景拍出可计算的整体。",
400
+ "page_grammar": "cinematic-feature",
401
+ "visual_focus": "客户图增强的上下对比图作主视觉,顶部 takeaway + 下方 3 张图横排",
402
+ "safe_text_zone": "top",
403
+ }
404
+ ]
405
+ },
406
+ ensure_ascii=False,
407
+ ),
408
+ encoding="utf-8",
409
+ )
410
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
411
+ (build / "img_plan.json").write_text(
412
+ json.dumps(
413
+ [
414
+ {
415
+ "file": "ai_p5.png",
416
+ "mode": "text-to-image",
417
+ "aspect": "16:9",
418
+ "prompt": "professional product photography of a sleek device",
419
+ }
420
+ ],
421
+ ensure_ascii=False,
422
+ ),
423
+ encoding="utf-8",
424
+ )
425
+ report = rewrite_plan(build)
426
+ self.assertEqual(report["changed_count"], 1)
427
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
428
+ prompt = plan[0]["prompt"]
429
+ self.assertNotIn("3 张", prompt)
430
+ self.assertNotIn("横排", prompt)
431
+ self.assertNotIn("对比图", prompt)
432
+ self.assertIn("complementary background", prompt)
433
+
434
+ def test_background_prompt_rejects_text_chip_labels(self):
435
+ # 2026-07-02 真机事故回归锁(ai_p23):design visual_focus 里的「三组'一'短标」漏过词表 →
436
+ # Seedream 照画三条带字药丸横在背景正中,与 HTML 标题叠字(deck 里根本没有这些字,全烧在图里)。
437
+ # 「短标/角标/清单」等带字元件词进 visual_focus/anchors 必须被拒收。
438
+ with tempfile.TemporaryDirectory() as d:
439
+ build = Path(d)
440
+ (build / "user_request.md").write_text("做产品详情页式 PPT", encoding="utf-8")
441
+ (build / "design.json").write_text(
442
+ json.dumps(
443
+ {
444
+ "pages": [
445
+ {
446
+ "page": 23,
447
+ "title": "让看不见无处遁形",
448
+ "one_sentence_takeaway": "看见,是一切质量的起点。",
449
+ "page_grammar": "hero-scene",
450
+ "visual_focus": "飞机翱翔云层航拍图 + 三组'一'短标",
451
+ "safe_text_zone": "left-center",
452
+ }
453
+ ]
454
+ },
455
+ ensure_ascii=False,
456
+ ),
457
+ encoding="utf-8",
458
+ )
459
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
460
+ (build / "img_plan.json").write_text(
461
+ json.dumps(
462
+ [
463
+ {
464
+ "file": "ai_p23.png",
465
+ "mode": "text-to-image",
466
+ "aspect": "16:9",
467
+ "prompt": "professional product photography of a sleek device",
468
+ }
469
+ ],
470
+ ensure_ascii=False,
471
+ ),
472
+ encoding="utf-8",
473
+ )
474
+ rewrite_plan(build)
475
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
476
+ prompt = plan[0]["prompt"]
477
+ self.assertNotIn("短标", prompt)
478
+
479
+ def test_background_prompt_does_not_copy_svg_timeline_layout(self):
480
+ with tempfile.TemporaryDirectory() as d:
481
+ build = Path(d)
482
+ (build / "user_request.md").write_text(
483
+ "用我电脑下载目录里面的航空器制造组装360度全景影像档案系统方案_副本 word 生成 PPT",
484
+ encoding="utf-8",
485
+ )
486
+ (build / "design.json").write_text(
487
+ json.dumps(
488
+ {
489
+ "pages": [
490
+ {
491
+ "page": 4,
492
+ "title": "三个根本性转变",
493
+ "one_sentence_takeaway": "360° 全景把工艺升级为完整时空档案。",
494
+ "page_grammar": "data-visual",
495
+ "visual_focus": "三个并列胶囊 + SVG 箭头时间线",
496
+ "safe_text_zone": "center",
497
+ }
498
+ ]
499
+ },
500
+ ensure_ascii=False,
501
+ ),
502
+ encoding="utf-8",
503
+ )
504
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
505
+ (build / "img_plan.json").write_text(
506
+ json.dumps(
507
+ [
508
+ {
509
+ "file": "ai_p4.png",
510
+ "mode": "text-to-image",
511
+ "aspect": "16:9",
512
+ "prompt": "professional product photography of a sleek device",
513
+ }
514
+ ],
515
+ ensure_ascii=False,
516
+ ),
517
+ encoding="utf-8",
518
+ )
519
+ report = rewrite_plan(build)
520
+ self.assertEqual(report["changed_count"], 1)
521
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
522
+ prompt = plan[0]["prompt"]
523
+ self.assertIn("三个根本性转变", prompt)
524
+ # 2026-07-03 起:背景锚剔除数字碎片与句子切片("360°""全景把工艺升级…"会被文字渲染型
525
+ # 模型画进图),场景名词锚(档案/影像档案系统方案)仍在、页面绑定不丢。
526
+ self.assertNotIn("360°", prompt)
527
+ self.assertIn("档案", prompt)
528
+ self.assertNotIn("时间线", prompt)
529
+ self.assertNotIn("胶囊", prompt)
530
+ self.assertNotIn("箭头", prompt)
531
+ # data-visual 页背景 = 纯氛围 + 留白,数据视觉声明由 HTML 层画。
532
+ self.assertIn("drawn by the HTML layer", prompt)
533
+ self.assertNotIn("Slide title:", prompt)
534
+ self.assertNotIn("Core message:", prompt)
535
+
536
+ def test_risky_design_fields_never_reach_prompt(self):
537
+ # 2026-07-01 真机事故回归锁:design 字段里的「行业缩略图」(经 Topic anchors)与
538
+ # 页标题里的裸「架构」(经 Slide title 拼接)曾被机器拼进 t2i prompt,被下游
539
+ # image_plan_lint 拦下 → 机器自产自拦、整条工作流确定性 halt。
540
+ # 现在 rewrite 与 lint 共享词表:这些词在拼接前被剥掉/拒收,产出必过 lint。
541
+ with tempfile.TemporaryDirectory() as d:
542
+ build = Path(d)
543
+ (build / "user_request.md").write_text("生成全景相机行业方案 PPT", encoding="utf-8")
544
+ (build / "design.json").write_text(
545
+ json.dumps(
546
+ {
547
+ "pages": [
548
+ {
549
+ "page": 8,
550
+ "title": "平台总体架构",
551
+ "one_sentence_takeaway": "一台全景相机覆盖 18+ 行业场景。",
552
+ "page_grammar": "hero-scene",
553
+ "visual_focus": "src_8 全景相机 + 18+ 行业落地 hero",
554
+ "visual_story": "上半用全景相机行业缩略图墙展示落地广度",
555
+ "safe_text_zone": "bottom",
556
+ }
557
+ ]
558
+ },
559
+ ensure_ascii=False,
560
+ ),
561
+ encoding="utf-8",
562
+ )
563
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
564
+ (build / "img_plan.json").write_text(
565
+ json.dumps(
566
+ [
567
+ {
568
+ "file": "ai_p8.png",
569
+ "mode": "text-to-image",
570
+ "aspect": "16:9",
571
+ "prompt": "professional product photography of a sleek device",
572
+ }
573
+ ],
574
+ ensure_ascii=False,
575
+ ),
576
+ encoding="utf-8",
577
+ )
578
+ report = rewrite_plan(build)
579
+ self.assertEqual(report["changed_count"], 1)
580
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
581
+ prompt = plan[0]["prompt"]
582
+ self.assertNotIn("缩略图", prompt)
583
+ self.assertNotIn("架构", prompt)
584
+ self.assertIn("全景相机", prompt)
585
+ # 集成兜底:改写后的 plan 直接过 lint(不带 outline,同 ppt.yaml 阶段11 调用形态)
586
+ violations, _, _ = lint(str(build / "img_plan.json"), None)
587
+ self.assertEqual(violations, [], violations)
588
+
589
+ def test_structural_original_prompt_recomposed_to_safe(self):
590
+ # LLM 原 prompt 自报结构图(decision tree / layered architecture / 对比图)→
591
+ # 不再靠下游 halt 等人修:机器直接重组成安全的页面绑定 prompt(「结构图回退」的确定性实现,
592
+ # 结构交 HTML 层、AI 图退化为安全底图),且重组产物必过 lint。
593
+ with tempfile.TemporaryDirectory() as d:
594
+ build = Path(d)
595
+ (build / "user_request.md").write_text("生成质量追溯平台 PPT", encoding="utf-8")
596
+ (build / "design.json").write_text(
597
+ json.dumps(
598
+ {
599
+ "pages": [
600
+ {
601
+ "page": 3,
602
+ "title": "证据链闭环",
603
+ "one_sentence_takeaway": "全流程留痕让追溯一步到位。",
604
+ "page_grammar": "hero-scene",
605
+ "visual_focus": "车间产线追溯场景",
606
+ "safe_text_zone": "left",
607
+ }
608
+ ]
609
+ },
610
+ ensure_ascii=False,
611
+ ),
612
+ encoding="utf-8",
613
+ )
614
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
615
+ (build / "img_plan.json").write_text(
616
+ json.dumps(
617
+ [
618
+ {
619
+ "file": "ai_p3.png",
620
+ "mode": "text-to-image",
621
+ "aspect": "16:9",
622
+ "prompt": "clean decision tree diagram with layered architecture, 对比图 style",
623
+ }
624
+ ],
625
+ ensure_ascii=False,
626
+ ),
627
+ encoding="utf-8",
628
+ )
629
+ report = rewrite_plan(build)
630
+ self.assertEqual(report["changed_count"], 1)
631
+ self.assertIn("risky_terms", report["changed"][0]["reason"])
632
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
633
+ prompt = plan[0]["prompt"]
634
+ self.assertNotIn("decision tree", prompt.lower())
635
+ self.assertNotIn("architecture", prompt.lower())
636
+ self.assertNotIn("对比图", prompt)
637
+ self.assertIn("证据链闭环", prompt)
638
+ violations, _, _ = lint(str(build / "img_plan.json"), None)
639
+ self.assertEqual(violations, [], violations)
640
+
641
+ def test_product_identity_anchor_appended_when_page_omits_subject(self):
642
+ # 2026-07-07 真机 p9 复盘:页面文案("安全性·分层防御")省略主语(不重复"全景相机"),
643
+ # 原 prompt 已含页面局部锚点(分层防御)+ 无风险词,此前 force_rewrite=False 直接放行,
644
+ # 完全不提客户真实产品外观,t2i 会画出无关设备。deck_meta.product_identity 必须被补上,
645
+ # 不依赖该页文案是否显式提及产品名。
646
+ with tempfile.TemporaryDirectory() as d:
647
+ build = Path(d)
648
+ (build / "user_request.md").write_text("航空器制造组装 360 度全景影像档案系统方案", encoding="utf-8")
649
+ (build / "design.json").write_text(
650
+ json.dumps(
651
+ {
652
+ "deck_meta": {
653
+ "product_identity": "a compact panoramic camera unit, matte black cylindrical housing with a red accent ring around the lens",
654
+ "customer_context": "Shenyang Taikeyi, serving aviation manufacturing clients",
655
+ },
656
+ "pages": [
657
+ {
658
+ "page": 9,
659
+ "title": "安全性 · 分层防御",
660
+ "one_sentence_takeaway": "安全是这套系统的一等约束,不是事后补丁",
661
+ "page_grammar": "spec-overlay",
662
+ "visual_focus": "硬件定制与物理隔离",
663
+ "safe_text_zone": "right",
664
+ }
665
+ ],
666
+ },
667
+ ensure_ascii=False,
668
+ ),
669
+ encoding="utf-8",
670
+ )
671
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
672
+ original_prompt = (
673
+ "a secure server rack inside an airplane cabin illustrating 分层防御 hardware isolation "
674
+ "with clean studio lighting and calm color grading"
675
+ )
676
+ (build / "img_plan.json").write_text(
677
+ json.dumps(
678
+ [{"file": "ai_p9.png", "mode": "text-to-image", "aspect": "16:9", "prompt": original_prompt}],
679
+ ensure_ascii=False,
680
+ ),
681
+ encoding="utf-8",
682
+ )
683
+ report = rewrite_plan(build)
684
+ self.assertEqual(report["changed_count"], 1, report)
685
+ self.assertIn("product_identity_anchor", report["changed"][0]["reason"])
686
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
687
+ prompt = plan[0]["prompt"]
688
+ self.assertIn("matte black cylindrical housing", prompt)
689
+ self.assertIn("Shenyang Taikeyi", prompt)
690
+
691
+ def test_product_identity_absent_is_backward_compatible(self):
692
+ # 没有 deck_meta(旧 design.json / 纯示意主题无真实产品照)时行为不变——不强行拼任何锚。
693
+ with tempfile.TemporaryDirectory() as d:
694
+ build = Path(d)
695
+ (build / "user_request.md").write_text("航空器制造组装 360 度全景影像档案系统方案", encoding="utf-8")
696
+ (build / "design.json").write_text(
697
+ json.dumps(
698
+ {
699
+ "pages": [
700
+ {
701
+ "page": 9,
702
+ "title": "安全性 · 分层防御",
703
+ "one_sentence_takeaway": "安全是这套系统的一等约束,不是事后补丁",
704
+ "page_grammar": "spec-overlay",
705
+ "visual_focus": "硬件定制与物理隔离",
706
+ "safe_text_zone": "right",
707
+ }
708
+ ]
709
+ },
710
+ ensure_ascii=False,
711
+ ),
712
+ encoding="utf-8",
713
+ )
714
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
715
+ original_prompt = (
716
+ "a secure server rack inside an airplane cabin illustrating 分层防御 hardware isolation "
717
+ "with clean studio lighting and calm color grading"
718
+ )
719
+ (build / "img_plan.json").write_text(
720
+ json.dumps(
721
+ [{"file": "ai_p9.png", "mode": "text-to-image", "aspect": "16:9", "prompt": original_prompt}],
722
+ ensure_ascii=False,
723
+ ),
724
+ encoding="utf-8",
725
+ )
726
+ report = rewrite_plan(build)
727
+ # deck_meta 缺失 → 只有原有的 append_negative_constraints 行为生效,不拼任何产品身份锚
728
+ # (这才是"行为不变":旧代码在这个 fixture 上也会因缺 NEGATIVE_OBJECTS 而报 changed=1)。
729
+ self.assertEqual(report["changed_count"], 1, report)
730
+ self.assertEqual(report["changed"][0]["reason"], "append_negative_constraints")
731
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
732
+ self.assertNotIn("Product visual consistency", plan[0]["prompt"])
733
+
734
+ def test_product_identity_risky_terms_stripped_before_landing_in_prompt(self):
735
+ # deck_meta.product_identity 是 LLM 写的自由文本,可能意外含 lint 黑名单词(如 architecture)——
736
+ # 落进 prompt 前必须剥净,保证「rewrite 产出必过 lint」的生产者契约不被 deck_meta 破坏。
737
+ with tempfile.TemporaryDirectory() as d:
738
+ build = Path(d)
739
+ (build / "user_request.md").write_text("测试主题", encoding="utf-8")
740
+ (build / "design.json").write_text(
741
+ json.dumps(
742
+ {
743
+ "deck_meta": {
744
+ "product_identity": "a device with a visible architecture diagram sticker, matte black housing",
745
+ },
746
+ "pages": [
747
+ {
748
+ "page": 3,
749
+ "title": "测试页",
750
+ "one_sentence_takeaway": "测试要点",
751
+ "page_grammar": "spec-overlay",
752
+ }
753
+ ],
754
+ },
755
+ ensure_ascii=False,
756
+ ),
757
+ encoding="utf-8",
758
+ )
759
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
760
+ (build / "img_plan.json").write_text(
761
+ json.dumps(
762
+ [{"file": "ai_p3.png", "mode": "text-to-image", "aspect": "16:9", "prompt": ""}],
763
+ ensure_ascii=False,
764
+ ),
765
+ encoding="utf-8",
766
+ )
767
+ rewrite_plan(build)
768
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
769
+ prompt = plan[0]["prompt"]
770
+ self.assertNotIn("architecture", prompt.lower())
771
+ self.assertIn("matte black housing", prompt)
772
+ violations, _, _ = lint(str(build / "img_plan.json"), None)
773
+ self.assertEqual(violations, [], violations)
774
+
775
+ def test_product_identity_included_in_force_rewritten_background_prompt(self):
776
+ # force_rewrite 路径(原 prompt 缺锚点,被整段重组)也要带上产品身份锚,
777
+ # 不只是"看起来还行、原样放行"的 tail 分支才补。
778
+ with tempfile.TemporaryDirectory() as d:
779
+ build = Path(d)
780
+ (build / "user_request.md").write_text("航空器制造组装 360 度全景影像档案系统方案", encoding="utf-8")
781
+ (build / "design.json").write_text(
782
+ json.dumps(
783
+ {
784
+ "deck_meta": {
785
+ "product_identity": "a compact panoramic camera unit, matte black cylindrical housing with a red accent ring around the lens",
786
+ },
787
+ "pages": [
788
+ {
789
+ "page": 9,
790
+ "title": "安全性 · 分层防御",
791
+ "one_sentence_takeaway": "安全是这套系统的一等约束,不是事后补丁",
792
+ "page_grammar": "spec-overlay",
793
+ "visual_focus": "硬件定制与物理隔离",
794
+ "safe_text_zone": "right",
795
+ }
796
+ ],
797
+ },
798
+ ensure_ascii=False,
799
+ ),
800
+ encoding="utf-8",
801
+ )
802
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
803
+ (build / "img_plan.json").write_text(
804
+ json.dumps(
805
+ [{"file": "ai_p9.png", "mode": "text-to-image", "aspect": "16:9", "prompt": ""}],
806
+ ensure_ascii=False,
807
+ ),
808
+ encoding="utf-8",
809
+ )
810
+ report = rewrite_plan(build)
811
+ self.assertEqual(report["changed_count"], 1, report)
812
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
813
+ prompt = plan[0]["prompt"]
814
+ self.assertIn("matte black cylindrical housing", prompt)
815
+
816
+ def test_second_run_is_idempotent_changed_zero(self):
817
+ # 幂等/真报:首跑重组后,二跑必须 changed_count==0 且 prompt 逐字节不变
818
+ # (旧实现 force_rewrite 无条件报 changed,真机二跑虚报 changed=12、报告失真)。
819
+ with tempfile.TemporaryDirectory() as d:
820
+ build = Path(d)
821
+ (build / "user_request.md").write_text("用知乎长文生成能源电网智能巡检平台 PPT", encoding="utf-8")
822
+ (build / "design.json").write_text(
823
+ json.dumps(
824
+ {
825
+ "pages": [
826
+ {
827
+ "page": 11,
828
+ "title": "多终端协同巡检",
829
+ "one_sentence_takeaway": "调度端、移动端和现场设备共享同一张电网运行图。",
830
+ "page_grammar": "product-closeup",
831
+ "visual_focus": "电网巡检终端协同查看运行态势",
832
+ "safe_text_zone": "left",
833
+ }
834
+ ]
835
+ },
836
+ ensure_ascii=False,
837
+ ),
838
+ encoding="utf-8",
839
+ )
840
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
841
+ (build / "img_plan.json").write_text(
842
+ json.dumps(
843
+ [
844
+ {
845
+ "file": "ai_p11.png",
846
+ "mode": "text-to-image",
847
+ "aspect": "16:9",
848
+ "prompt": "professional product photography of a sleek device on a dark background",
849
+ }
850
+ ],
851
+ ensure_ascii=False,
852
+ ),
853
+ encoding="utf-8",
854
+ )
855
+ first = rewrite_plan(build)
856
+ self.assertEqual(first["changed_count"], 1)
857
+ plan_after_first = (build / "img_plan.json").read_text(encoding="utf-8")
858
+ second = rewrite_plan(build)
859
+ self.assertEqual(second["changed_count"], 0, second)
860
+ self.assertEqual((build / "img_plan.json").read_text(encoding="utf-8"), plan_after_first)
861
+
862
+ def test_product_identity_anchor_second_run_is_idempotent(self):
863
+ # 带 deck_meta.product_identity 时同样必须幂等——二跑不重复拼锚、不虚报 changed。
864
+ with tempfile.TemporaryDirectory() as d:
865
+ build = Path(d)
866
+ (build / "user_request.md").write_text("航空器制造组装 360 度全景影像档案系统方案", encoding="utf-8")
867
+ (build / "design.json").write_text(
868
+ json.dumps(
869
+ {
870
+ "deck_meta": {
871
+ "product_identity": "a compact panoramic camera unit, matte black cylindrical housing with a red accent ring around the lens",
872
+ },
873
+ "pages": [
874
+ {
875
+ "page": 9,
876
+ "title": "安全性 · 分层防御",
877
+ "one_sentence_takeaway": "安全是这套系统的一等约束,不是事后补丁",
878
+ "page_grammar": "spec-overlay",
879
+ }
880
+ ],
881
+ },
882
+ ensure_ascii=False,
883
+ ),
884
+ encoding="utf-8",
885
+ )
886
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
887
+ (build / "img_plan.json").write_text(
888
+ json.dumps(
889
+ [{"file": "ai_p9.png", "mode": "text-to-image", "aspect": "16:9", "prompt": ""}],
890
+ ensure_ascii=False,
891
+ ),
892
+ encoding="utf-8",
893
+ )
894
+ first = rewrite_plan(build)
895
+ self.assertEqual(first["changed_count"], 1, first)
896
+ plan_after_first = (build / "img_plan.json").read_text(encoding="utf-8")
897
+ second = rewrite_plan(build)
898
+ self.assertEqual(second["changed_count"], 0, second)
899
+ self.assertEqual((build / "img_plan.json").read_text(encoding="utf-8"), plan_after_first)
900
+
901
+ def test_background_prompt_never_carries_slide_copy_ai_p9_regression(self):
902
+ # 2026-07-03 ai_p9 真机事故回归锁:背景 t2i prompt 曾拼进页标题/带金额的正文句/图表描述,
903
+ # Seedream 类文字渲染型模型照着画出一整版烘字假幻灯片(投入有限对冲无限 + 成本清单 + 柱状图),
904
+ # 与 HTML 真表格/真图表互相遮挡。背景层提示词必须去内容化,且产出必过 lint 新增 slide_copy 检查。
905
+ with tempfile.TemporaryDirectory() as d:
906
+ build = Path(d)
907
+ (build / "user_request.md").write_text("生成航空制造全景影像方案 PPT", encoding="utf-8")
908
+ (build / "design.json").write_text(
909
+ json.dumps(
910
+ {
911
+ "pages": [
912
+ {
913
+ "page": 9,
914
+ "title": "投入有限,对冲无限",
915
+ "one_sentence_takeaway": "核心总投入约 3 万元,相当于单架飞机潜在事故成本的对冲。",
916
+ "page_grammar": "data-visual",
917
+ "visual_focus": "成本拆解 HTML 表 + 投入/对冲对比柱状图",
918
+ "safe_text_zone": "center",
919
+ }
920
+ ]
921
+ },
922
+ ensure_ascii=False,
923
+ ),
924
+ encoding="utf-8",
925
+ )
926
+ (build / "source_layout_plan.json").write_text("{}", encoding="utf-8")
927
+ # 旧模板产物原样作为输入(含 Slide title/Core message/柱状图描述)——必须被强制重组。
928
+ (build / "img_plan.json").write_text(
929
+ json.dumps(
930
+ [
931
+ {
932
+ "file": "ai_p9.png",
933
+ "mode": "text-to-image",
934
+ "aspect": "16:9",
935
+ "target_layer": "background",
936
+ "prompt": (
937
+ "low-interference data-space background. Slide title: 投入有限,对冲无限. "
938
+ "Core message: 核心总投入约 3 万元,相当于单架飞机潜在事故成本的对冲. "
939
+ "Topic anchors: 投入有限, 核心总投入约, 万元. "
940
+ "Visual focus: 成本拆解 HTML 表 + 投入/对冲对比柱状图. no text, no logo."
941
+ ),
942
+ }
943
+ ],
944
+ ensure_ascii=False,
945
+ ),
946
+ encoding="utf-8",
947
+ )
948
+ report = rewrite_plan(build)
949
+ self.assertEqual(report["changed_count"], 1)
950
+ self.assertIn("slide_copy_in_background_prompt", report["changed"][0]["reason"])
951
+ plan = json.loads((build / "img_plan.json").read_text(encoding="utf-8"))
952
+ prompt = plan[0]["prompt"]
953
+ self.assertNotIn("Slide title:", prompt)
954
+ self.assertNotIn("Core message:", prompt)
955
+ self.assertNotIn("Topic anchors:", prompt)
956
+ self.assertNotIn("3 万元", prompt)
957
+ self.assertNotIn("柱状图", prompt)
958
+ self.assertNotIn("万元", prompt)
959
+ self.assertIn("no charts", prompt)
960
+ # 生产者-闸门契约:重组产物必过 lint(含新增 slide_copy 背景检查)。
961
+ violations, _, _ = lint(str(build / "img_plan.json"), None)
962
+ self.assertEqual(violations, [], violations)
963
+
964
+
965
+ if __name__ == "__main__":
966
+ unittest.main(verbosity=2)