minimal-agent 0.10.0 → 0.12.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 (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
@@ -0,0 +1,1220 @@
1
+ /**
2
+ * test/ppt.load.test.ts
3
+ * ------------------------------------------------------------
4
+ * 锁定 workflows/ppt.yaml(统一素材摄取 → 富化中间 material → 素材驱动版,与 webppt 并列的独立工作流)的
5
+ * 「可解析 + 关键骨架」:
6
+ * - name=ppt(不是 webppt)、22 个顶层步、title 分母 /22
7
+ * - inputs 含 topic(greedy 首位) + source(任意素材路径)+ style/mode/illustrations/out/image_intensity
8
+ * - 摄取三步:stage_extract(抽文字+图片 + 图像统计)→ stage_vision_loop(for-each over readJson 逐图识图)
9
+ * → stage_assemble_material(合并 material.md/json)
10
+ * - 素材驱动:draft_from_material 以 user_request.md + material.md 为主干、design_doc 读 material.json(客户图按识图内容高权重 route=source/source-enhanced、必须全用)、
11
+ * review 查客户图全用 + 识图对齐 + 配色协调、build 客户图 contain 不裁切 + 按朝向/重点区 + 客户图用尽闸门
12
+ * - 所有 ${} 表达式可被引擎解析
13
+ *
14
+ * 定位法:packageRoot/workflows/ppt.yaml + parseWorkflowFile(ppt 与 webppt 是两个不同名工作流)。
15
+ */
16
+
17
+ import { describe, expect, it } from 'bun:test';
18
+ import { join } from 'node:path';
19
+
20
+ import { evalExpr, interpolate } from '../src/expressions.js';
21
+ import { parseWorkflowFile } from '../src/loader.js';
22
+ import { VarStack } from '../src/types.js';
23
+ import type { StepDef } from '../src/types.js';
24
+
25
+ const pptPath = join(
26
+ import.meta.dir.replace(/[/\\]test$/, ''), // plugins/workflow-runner
27
+ '..', // plugins
28
+ '..', // 仓库根
29
+ 'workflows',
30
+ 'ppt.yaml',
31
+ );
32
+
33
+ describe('workflows/ppt.yaml(统一素材摄取·素材驱动)', () => {
34
+ it('解析通过且 name=ppt、25 步骨架正确', async () => {
35
+ const def = await parseWorkflowFile(pptPath);
36
+
37
+ expect(def.name).toBe('ppt'); // 与 webppt 区分,不撞名
38
+ expect(typeof def.description).toBe('string');
39
+
40
+ // inputs:topic(greedy 首位) + source 素材 + 高级覆盖项
41
+ const inputNames = (def.inputs ?? []).map((i) => i.name);
42
+ expect(inputNames).toEqual(
43
+ expect.arrayContaining(['topic', 'source', 'illustrations', 'style', 'mode', 'out', 'image_intensity']),
44
+ );
45
+ expect(inputNames[0]).toBe('topic');
46
+ expect((def.inputs ?? []).find((i) => i.name === 'topic')?.greedy).toBe(true);
47
+ const source = (def.inputs ?? []).find((i) => i.name === 'source');
48
+ expect(source?.default).toBe(''); // 可选,留空自动扫 cwd
49
+
50
+ // 22 个顶层步(含生成图语义复核、复核汇总与语义失败重试)+ title 分母 /22
51
+ const ids = def.steps.map((s) => s.id);
52
+ for (const id of [
53
+ 'stage0_clarify',
54
+ 'stage_extract',
55
+ 'stage_vision_loop',
56
+ 'stage_assemble_material',
57
+ 'stage_draft_from_material',
58
+ 'stage_draft_coverage',
59
+ 'stage_draft_coverage_repair',
60
+ 'stage_design_doc',
61
+ 'stage_design_json_gate',
62
+ 'stage_design_review',
63
+ 'stage_design_recheck',
64
+ 'stage_design_repair',
65
+ 'stage_img_route_gate',
66
+ 'stage_gen_images',
67
+ 'stage_image_quality_gate',
68
+ 'stage_generated_image_review',
69
+ 'stage_generated_image_review_collect',
70
+ 'stage_generated_image_retry',
71
+ 'stage_build_loop',
72
+ 'stage_visual_quality_gate',
73
+ 'stage_export',
74
+ 'verify_output',
75
+ 'cleanup_intermediates',
76
+ 'present_result',
77
+ 'final_report',
78
+ ]) {
79
+ expect(ids).toContain(id);
80
+ }
81
+ expect(def.steps.length).toBe(25);
82
+ expect(ids.indexOf('cleanup_intermediates')).toBeLessThan(ids.indexOf('present_result'));
83
+ expect(ids.indexOf('present_result')).toBeLessThan(ids.indexOf('final_report'));
84
+ expect(ids.indexOf('stage_draft_from_material')).toBeLessThan(ids.indexOf('stage_draft_coverage'));
85
+ expect(ids.indexOf('stage_draft_coverage')).toBeLessThan(ids.indexOf('stage_draft_coverage_repair'));
86
+ expect(ids.indexOf('stage_draft_coverage_repair')).toBeLessThan(ids.indexOf('stage_design_doc'));
87
+ expect(ids.indexOf('stage_design_doc')).toBeLessThan(ids.indexOf('stage_design_json_gate'));
88
+ expect(ids.indexOf('stage_design_json_gate')).toBeLessThan(ids.indexOf('stage_img_route_gate'));
89
+ expect(ids.indexOf('stage_img_route_gate')).toBeLessThan(ids.indexOf('stage_build_loop'));
90
+ expect(ids.indexOf('stage_gen_images')).toBeLessThan(ids.indexOf('stage_build_loop'));
91
+ expect(ids.indexOf('stage_image_quality_gate')).toBeLessThan(ids.indexOf('stage_build_loop'));
92
+ for (const s of def.steps) {
93
+ expect(String(s.title)).toContain('/22');
94
+ }
95
+
96
+ // 收尾 echo;artifacts 只列最终交付物、不在被 rm 的 ./build 下
97
+ expect(def.steps.find((s) => s.id === 'final_report')?.type).toBe('echo');
98
+ const declared = def.artifacts ?? [];
99
+ expect(declared).toContain('./out/deck.pptx');
100
+ for (const a of declared) expect(a.startsWith('./build')).toBe(false);
101
+ });
102
+
103
+ it('stage0_clarify:样式推荐按配色感知、覆盖全部 13 个 VI(不再只露 6 个)', async () => {
104
+ const def = await parseWorkflowFile(pptPath);
105
+ const s0 = def.steps.find((s) => s.id === 'stage0_clarify');
106
+ const p = String(s0?.agent);
107
+ // 配色感知:显式要求把「行业/情绪/品牌色」映射到模板配色再推,没说偏好按行业惯例配色兜底
108
+ expect(p).toContain('配色');
109
+ expect(p).toContain('行业惯例配色');
110
+ expect(p).toMatch(/主色调|底色|色系/);
111
+ // 全部 13 个内置 VI 都进推荐菜单(此前只露 6 个:verdant/aegis/ember/amber/obsidian-xui/precision 不可见)
112
+ for (const vi of [
113
+ 'aqua-glass',
114
+ 'violet-meridian',
115
+ 'ember-prism',
116
+ 'amber-prism',
117
+ 'verdant-grid',
118
+ 'aegis-vector',
119
+ 'comfort-vision',
120
+ 'nebula-console',
121
+ 'obsidian-xui',
122
+ 'obsidian-optics',
123
+ 'precision-horizon-dark',
124
+ 'solar-relay',
125
+ ]) {
126
+ expect(p).toContain(vi);
127
+ }
128
+ // precision-horizon(浅色)独立成条,用全角冒号锁定、与 -dark 区分
129
+ expect(p).toContain('precision-horizon:');
130
+ // 指向共享技能目录的配色全表,供需要时深读
131
+ expect(p).toContain('视觉模板库');
132
+ // 「样式必问用户」红线 + AskUserQuestion 仍在
133
+ expect(p).toContain('AskUserQuestion');
134
+ expect(s0?.allowed_tools ?? []).toContain('AskUserQuestion');
135
+ });
136
+
137
+ it('stage_extract:抽取脚本 + 图像统计脚本,产 source_assets.json/source_stats.json,无素材不阻断', async () => {
138
+ const def = await parseWorkflowFile(pptPath);
139
+ const ex = def.steps.find((s) => s.id === 'stage_extract');
140
+ expect(ex).toBeTruthy();
141
+ const a = String(ex?.args?.command);
142
+ expect(ex?.tool).toBe('Bash');
143
+ expect(a).toContain('user_request.md'); // 早期固化用户目标
144
+ expect(a).toContain('resolve_source_input.py'); // 自然语言素材路径解析
145
+ expect(a).toContain('source_resolution.json'); // 解析报告
146
+ expect(a).toContain('PPT_SOURCE_REQUIRED_BUT_EMPTY'); // 明确素材缺失不伪装纯 topic
147
+ expect(a).toContain('extract_source_assets.py'); // 确定性抽取脚本
148
+ expect(a).toContain('image_stats.py'); // 图像统计脚本(image-stats skill)
149
+ expect(a).toContain('source_assets.json'); // base manifest
150
+ expect(a).toContain('source_stats.json'); // 统计产物
151
+ expect(a).toContain('${inputs.source}'); // 显式 source 优先
152
+ expect(a).toContain('--scan'); // 留空时扫 cwd
153
+ expect(ex?.onError).toBe('halt');
154
+ // 顺序:clarify → extract → vision → assemble → draft
155
+ const ids = def.steps.map((s) => s.id);
156
+ expect(ids.indexOf('stage_extract')).toBeGreaterThan(ids.indexOf('stage0_clarify'));
157
+ expect(ids.indexOf('stage_extract')).toBeLessThan(ids.indexOf('stage_vision_loop'));
158
+ expect(ids.indexOf('stage_vision_loop')).toBeLessThan(ids.indexOf('stage_assemble_material'));
159
+ expect(ids.indexOf('stage_assemble_material')).toBeLessThan(ids.indexOf('stage_draft_from_material'));
160
+ });
161
+
162
+ it('stage_vision_loop:for-each over readJson 遍历真实图片清单,逐图多模态识图写 vision json', async () => {
163
+ const def = await parseWorkflowFile(pptPath);
164
+ const loop = def.steps.find((s) => s.id === 'stage_vision_loop');
165
+ expect(loop?.type).toBe('loop');
166
+ // 用 asArray(readJson(...)) 取出运行时抽取的图片清单;缺字段/null → 0 轮,坏结构由 asArray 报错。
167
+ expect(String(loop?.over)).toContain('asArray');
168
+ expect(String(loop?.over)).toContain('readJson');
169
+ expect(String(loop?.over)).toContain('source_assets.json');
170
+ expect(String(loop?.over)).toContain('images');
171
+ expect(loop?.as).toBe('img');
172
+ expect(String(loop?.when)).toContain('source_assets.json'); // 无 manifest 不进
173
+ // 内层 vision_one:多模态 llm,images 引用当前 img.file;随后 Write 落盘
174
+ const dos = (loop?.do ?? []) as StepDef[];
175
+ const vone = dos.find((s) => s.id === 'vision_one');
176
+ expect(vone).toBeTruthy();
177
+ expect(Array.isArray(vone?.images)).toBe(true);
178
+ expect(String((vone?.images ?? []).join(','))).toContain('${img.file}'); // 喂当前图给模型
179
+ const va = String(vone?.llm);
180
+ expect(va).toContain('key_region'); // 重点区
181
+ expect(va).toContain('suitable_as_background'); // 是否宜做背景
182
+ // 2026-07-03:图表核心结论字段(治「图表寓意没被理解、配不上」)
183
+ expect(va).toContain('key_message');
184
+ expect(va).toContain('data_points');
185
+ // 2026-07-03:图上烧进画面的文字结构化记录(治「带字图直接图生图,字既没去掉也没被记下来重配」)
186
+ expect(va).toContain('embedded_text');
187
+ expect(va).toContain('region');
188
+ // 真机复盘:模型把文字写进 description 却漏填 embedded_text 结构化字段(key_message/data_points 都填了)
189
+ // ——2026-07-03 提升显著性,要求 description 提到的文字必须同步抄进 embedded_text。
190
+ expect(va).toContain('必填此字段');
191
+ expect(va).toContain('原样再抄一遍');
192
+ expect(vone?.onError).toBe('continue'); // 无多模态模型 / 单图失败不阻断
193
+ const write = dos.find((s) => s.id === 'write_vision_json');
194
+ expect(write?.tool).toBe('Write');
195
+ expect(String(write?.args?.file_path)).toContain('vision/${img.file}.json');
196
+ expect(String(write?.when)).toContain('length(vision_json)');
197
+ });
198
+
199
+ it('stage_draft_coverage(_repair):原文主题覆盖检查 + fast/best 都跑的单次回填(2026-07-03)', async () => {
200
+ const def = await parseWorkflowFile(pptPath);
201
+ const check = def.steps.find((s) => s.id === 'stage_draft_coverage');
202
+ expect(check?.tool).toBe('Bash');
203
+ expect(String(check?.args?.command)).toContain('draft_coverage_check.py');
204
+ expect(check?.onError).toBe('continue');
205
+
206
+ const repair = def.steps.find((s) => s.id === 'stage_draft_coverage_repair');
207
+ expect(String(repair?.when)).toContain('draft_coverage_report.json');
208
+ expect(String(repair?.when)).toContain("readJson('./build/draft_coverage_report.json', 'ok') == false");
209
+ // 用户点名最高优先级问题:fast/best 都要跑,when 不能限死 quality=='best'。
210
+ expect(String(repair?.when)).not.toContain("quality == 'best'");
211
+ expect(String(repair?.agent)).toContain('draft_coverage_report.md');
212
+ expect(String(repair?.agent)).toContain('material.md');
213
+ expect(String(repair?.agent)).toContain('尽力而为的单次回填');
214
+ expect(repair?.onError).toBe('continue');
215
+ expect(repair?.max_retries).toBe(0);
216
+ });
217
+
218
+ it('2026-07-03 四页复盘:版式规划执行契约 + 背景前景禁同主体重复(第3/4页事故)', async () => {
219
+ const def = await parseWorkflowFile(pptPath);
220
+ const dos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
221
+ const batch = String(dos.find((s) => s.id === 'build_batch')?.agent);
222
+ // ① build_batch 客户图引用契约:contain 类图禁铺整页背景,且明示视觉门有确定性【版式违规】兜底
223
+ // (src_6 识图明说"绝不能当背景"、规划判 direct_contain,build 仍铺成 .bg-img——prompt 拦不住,
224
+ // 真修在 source_usage_check --layout-intent-only 的可修档 ERROR,这里只是"写对省一轮"提示)。
225
+ expect(batch).toContain('版式违规');
226
+ expect(batch).toContain('绝不铺整页背景');
227
+ // ② build_batch 同主体禁重复推广到任意图源(背景驾驶舱+前景 3 张驾驶舱特写 = 第3页事故)。
228
+ expect(batch).toContain('前景禁止再排同主体图片卡');
229
+ // ③ design_doc 一页一主视觉规则。
230
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
231
+ expect(dd).toContain('背景前景禁同主体重复');
232
+ expect(dd).toContain('一页一主视觉');
233
+ // ④ design_review 罪状清单质问项。
234
+ const rv = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
235
+ expect(rv).toContain('背景前景同主体重复核对');
236
+ });
237
+
238
+ it('2026-07-03 六页复盘:角标/标签强调色必须自带色块底(--arsenal-blue 系统性靠色事故)', async () => {
239
+ const def = await parseWorkflowFile(pptPath);
240
+ const dos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
241
+ const batch = String(dos.find((s) => s.id === 'build_batch')?.agent);
242
+ // build_batch 预防性规则:t-eyebrow/label 类强调色短文字不得裸色浮在不确定背景上。
243
+ expect(batch).toContain('角标/标签强调色文字必须自带色块底');
244
+ expect(batch).toContain('t-eyebrow');
245
+ expect(batch).toContain('bullet-row .label');
246
+ // design_review 罪状清单对应质问项。
247
+ const rv = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
248
+ expect(rv).toContain('强调色角标/标签系统性靠色核对');
249
+ });
250
+
251
+ it('2026-07-04 真机复盘:split 左图右文页 .edit-scrim 渐变方向必须对齐文字栏(p3/p7 同根因事故)', async () => {
252
+ const def = await parseWorkflowFile(pptPath);
253
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
254
+ // build_batch 预防性规则:写 scrim 前先看文字在哪一栏,不能照抄示例的「左不透明右透明」方向。
255
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
256
+ expect(batch).toContain('渐变方向必须对齐文字实际所在的栏');
257
+ expect(batch).toContain('镜像');
258
+ // visual_repair_batch 修复时的识别规则:整段正文压图不可读时先查渐变方向,别逐行打局部 scrim 补丁。
259
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
260
+ const visualRepair = gateDos.find((s) => s.id === 'visual_repair_batch');
261
+ expect(String(visualRepair?.agent)).toContain('跟着文字栏镜像');
262
+ // 2026-07-05 复盘:镜像方向对了但字色没跟着换(深底深字)——scrim 深浅变体必须跟文字色绑定。
263
+ expect(batch).toContain('深浅 scrim 变体必须跟文字色绑定');
264
+ expect(batch).toContain('浅色 VI 模板(白底+深字,如 aegis-vector 系)优先选浅色磨砂镜像变体');
265
+ });
266
+
267
+ it('2026-07-05 真机复盘:.slide.section 章节引导页同样要镜像 scrim(p2/p5 同根因事故,上一版规则被误读成只对 split/two-col 生效)', async () => {
268
+ const def = await parseWorkflowFile(pptPath);
269
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
270
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
271
+ expect(batch).toContain('.slide.section');
272
+ expect(batch).toContain('唯一判据是文字所在的 `grid-column`,不是 class 名字');
273
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
274
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
275
+ expect(visualRepair).toContain('.slide.section');
276
+ expect(visualRepair).toContain('判据只看文字实际的');
277
+ });
278
+
279
+ it('2026-07-05 真机复盘:图渐隐入纸感面板式 scrim 的不透明拐点必须早于文字栏起点(p4/p14 同根因,正文发糊但标题正常)', async () => {
280
+ const def = await parseWorkflowFile(pptPath);
281
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
282
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
283
+ expect(batch).toContain('图渐隐入纸感面板');
284
+ expect(batch).toContain('渐变必须在这个百分比之前就已经到达 ≥0.85 不透明度');
285
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
286
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
287
+ expect(visualRepair).toContain('大概率不是方向错,是"图渐隐入纸感面板"式 scrim 的不透明拐点太晚');
288
+ });
289
+
290
+ it('2026-07-05 真机复盘:scrim 不透明区必须覆盖文字所在区域不分横竖轴(light-bottom 类页顶标题落空事故)', async () => {
291
+ const def = await parseWorkflowFile(pptPath);
292
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
293
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
294
+ expect(batch).toContain('不分横竖轴、不分具体 CSS 写法');
295
+ expect(batch).toContain('light-bottom');
296
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
297
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
298
+ expect(visualRepair).toContain('若不可读的是 `.deck-header`/`.deck-title`');
299
+ });
300
+
301
+ it('2026-07-05 真机复盘:卡内文字禁用 margin-top:auto 等边缘对齐撑出空心大卡(四重防线安全矩阵页事故)', async () => {
302
+ const def = await parseWorkflowFile(pptPath);
303
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
304
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
305
+ expect(batch).toContain('margin-top:auto');
306
+ expect(batch).toContain('align-self:flex-end');
307
+ expect(batch).toContain('四重防线安全矩阵页实测事故');
308
+ });
309
+
310
+ it('2026-07-05 真机复盘:内容块 auto 高度浮在 1fr body 行顶部导致下半页整页留白(链式价值页事故)', async () => {
311
+ const def = await parseWorkflowFile(pptPath);
312
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
313
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
314
+ expect(batch).toContain('grid-template-rows:auto 1fr');
315
+ expect(batch).toContain('链式价值');
316
+ expect(batch).toContain('不能让一组内容卡浮在页面上方、下半页整页留白');
317
+ });
318
+
319
+ it('2026-07-05 真机复盘:超大号单行标题渲染宽度超出共享 scrim 固定拐点百分比(p3「一次代价高昂的质量事故」事故)', async () => {
320
+ const def = await parseWorkflowFile(pptPath);
321
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
322
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
323
+ expect(batch).toContain('scrim-left-dark');
324
+ expect(batch).toContain('字符数 × 字号');
325
+ expect(batch).toContain('方向和拐点位置都对');
326
+ });
327
+
328
+ it('2026-07-05 真机复盘:SVG柱状图短柱塞不下自己的内嵌标签导致溢出侵入邻柱(p10成本条形图事故)', async () => {
329
+ const def = await parseWorkflowFile(pptPath);
330
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
331
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
332
+ expect(batch).toContain('查看基础·4–5千');
333
+ expect(batch).toContain('画在柱子**外侧**');
334
+ });
335
+
336
+ it('2026-07-05 真机复盘:align-self:center撑爆画布把标题挤出可见区域(p6五大暗角事故)', async () => {
337
+ const def = await parseWorkflowFile(pptPath);
338
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
339
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
340
+ expect(batch).toContain('比溢出到底部更隐蔽更严重');
341
+ expect(batch).toContain('bullet-strip');
342
+ });
343
+
344
+ it('2026-07-05 真机复盘:径向图表外围标签延伸方向超出viewBox边界被裁切(p6五角图事故)', async () => {
345
+ const def = await parseWorkflowFile(pptPath);
346
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
347
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
348
+ expect(batch).toContain('时间碎片');
349
+ expect(batch).toContain('viewBox="0 0 600 600"');
350
+ expect(batch).toContain('text-anchor:start');
351
+ });
352
+
353
+ it('2026-07-05 真机复盘:图+说明卡片行不能各自独立占用同一 grid-column/grid-row(第8页74~78%重叠事故)', async () => {
354
+ const def = await parseWorkflowFile(pptPath);
355
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
356
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
357
+ expect(batch).toContain('图片和它下方的说明卡片被写成两个独立');
358
+ expect(batch).toContain('包进同一个 `<div style="grid-column:1/2;grid-row:2/3;display:flex;flex-direction:column">`');
359
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
360
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
361
+ expect(visualRepair).toContain('一张图」和「其下方说明卡片行」且重叠占比 >70%');
362
+ });
363
+
364
+ it('2026-07-05 真机复盘:.fig-contain/.fig-fill 禁止内联 aspect-ratio(第6/8页事故,撑爆1fr轨道致catastrophic硬halt)', async () => {
365
+ const def = await parseWorkflowFile(pptPath);
366
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
367
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
368
+ expect(batch).toContain('这次是 `.fig-contain` 共享组件被内联 `aspect-ratio` 覆盖');
369
+ expect(batch).toContain('任何 `.fig-contain`/`.fig-fill` 组件都不准在内联 style 里加 `aspect-ratio`');
370
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
371
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
372
+ expect(visualRepair).toContain('即使文字前缀写的是"WARNING"也必须排进本批最优先修');
373
+ expect(visualRepair).toContain('删掉这个内联 `aspect-ratio`');
374
+ });
375
+
376
+ it('2026-07-05 真机复盘:水平柱图满刻度柱的数值标签需画在柱内且换高对比色(ROI页事故,标签色=柱色致对比度1.00)', async () => {
377
+ const def = await parseWorkflowFile(pptPath);
378
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
379
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
380
+ expect(batch).toContain('柱长 ≥ 绘图区宽度 90%');
381
+ expect(batch).toContain('柱内部靠右');
382
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
383
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
384
+ expect(visualRepair).toContain('柱长≥绘图区 90% 时标签必须画在柱内靠右');
385
+ });
386
+
387
+ it('2026-07-05 真机复盘:image_weight≈0 的 data-visual/list/metric 页不得用近纯白整页 scrim 盖死 AI 背景图(13页deck 4页同时中招,图形同虚设)', async () => {
388
+ const def = await parseWorkflowFile(pptPath);
389
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
390
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
391
+ expect(batch).toContain('image_weight` 判定接近 0 的 data-visual/list/metric 结构页');
392
+ expect(batch).toContain('不得超过约 0.6');
393
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
394
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
395
+ expect(visualRepair).toContain('AI 背景图被 scrim 盖到不可辨识');
396
+ });
397
+
398
+ it('2026-07-06 真机复盘:圆形徽标(.ring-label/.step-num)塞长文本会溢出圆外与邻近内容重叠,需改胶囊/矩形(N周→N小时、6–12月两页同时中招)', async () => {
399
+ const def = await parseWorkflowFile(pptPath);
400
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
401
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
402
+ expect(batch).toContain('圆形徽标/环形进度条内嵌文字');
403
+ expect(batch).toContain('横向胶囊/矩形标签');
404
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
405
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
406
+ expect(visualRepair).toContain('圆形徽标塞长文本溢出');
407
+ });
408
+
409
+ it('2026-07-06 真机复盘:build_batch 绝不能编辑 img_plan.json(阶段7生图已跑完,事后加条目不会真的生成,留下幽灵引用致source_usage_check硬门halt整个-p进程)', async () => {
410
+ const def = await parseWorkflowFile(pptPath);
411
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
412
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
413
+ expect(batch).toContain('绝不修改 `img_plan.json`');
414
+ expect(batch).toContain('`img_plan.json` 只能由阶段 7 之前的确定性脚本步骤写');
415
+ });
416
+
417
+ it('2026-07-05 真机复盘:内容页 deck-header 叠在 AI 照片背景上同样要对比度保护,不止封面/章节(p5 三大盲区标题实测对比度1.00事故)', async () => {
418
+ const def = await parseWorkflowFile(pptPath);
419
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
420
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
421
+ expect(batch).toContain('同一条铁律对所有内容页同样生效,不止封面/章节');
422
+ expect(batch).toContain('像素实测最差对比度 1.00,标题实质不可读');
423
+ });
424
+
425
+ it('2026-07-05 真机复盘:.brand-mark 角标改色治标不治本,必须照抄 .t-eyebrow 加背景色块(本轮已改深色仍8处对比度1.00事故)', async () => {
426
+ const def = await parseWorkflowFile(pptPath);
427
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
428
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
429
+ expect(visualRepair).toContain('.brand-mark');
430
+ expect(visualRepair).toContain('纯改文字颜色治标不治本');
431
+ });
432
+
433
+ it('2026-07-05 真机复盘:过小字体共享同一错误数值时repair要一次性批量替换,不逐条改(10处font-size:22px事故)', async () => {
434
+ const def = await parseWorkflowFile(pptPath);
435
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
436
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
437
+ expect(visualRepair).toContain('共享同一数值 font-size:Npx');
438
+ expect(visualRepair).toContain('批量把这个错误数值');
439
+ });
440
+
441
+ it('2026-07-05 真机复盘:route=source-enhanced页不适用ai_p<page>.png约定名(7页事故,阶段9对这类页永不生成该文件)', async () => {
442
+ const def = await parseWorkflowFile(pptPath);
443
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
444
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
445
+ expect(batch).toContain('绝不适用上面②这条约定名规则');
446
+ expect(batch).toContain('直接留空即可');
447
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
448
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
449
+ expect(visualRepair).toContain('永远不会存在');
450
+ expect(visualRepair).toContain('绝不改猜其它文件名重试');
451
+ });
452
+
453
+ it('2026-07-05 真机复盘:给深底文字转白别靠text-white单类工具类,可能被更高特异性选择器覆盖(5页slide.section章节页事故)', async () => {
454
+ const def = await parseWorkflowFile(pptPath);
455
+ const buildDos = def.steps.find((s) => s.id === 'stage_build_loop')?.do ?? [];
456
+ const batch = String(buildDos.find((s) => s.id === 'build_batch')?.agent);
457
+ expect(batch).toContain('特异性 (0,3,0)');
458
+ expect(batch).toContain('用内联 `style="color:#fff"`');
459
+ const gateDos = def.steps.find((s) => s.id === 'stage_visual_quality_gate')?.do ?? [];
460
+ const visualRepair = String(gateDos.find((s) => s.id === 'visual_repair_batch')?.agent);
461
+ expect(visualRepair).toContain('更高特异性的选择器');
462
+ expect(visualRepair).toContain('内联 `style="color:#fff"`');
463
+ });
464
+
465
+ it('stage_assemble_material:合并脚本产 material.md/json + 确定性客户图版式/图生图规划', async () => {
466
+ const def = await parseWorkflowFile(pptPath);
467
+ const am = def.steps.find((s) => s.id === 'stage_assemble_material');
468
+ const a = String(am?.args?.command);
469
+ expect(am?.tool).toBe('Bash');
470
+ expect(a).toContain('assemble_material.py');
471
+ expect(a).toContain('material.json');
472
+ expect(a).toContain('material.md');
473
+ // 紧接确定性规划脚本:逐图算定 usage_intent/是否图生图/模板/文字区 → source_layout_plan.json/md
474
+ expect(a).toContain('plan_source_layout.py');
475
+ expect(a).toContain('source_layout_plan');
476
+ expect(am?.onError).toBe('continue');
477
+ });
478
+
479
+ it('客户图图生图确定性化:规划→设计服从→兜底注入 img_plan,治「低清图硬铺/图生图没触发」根因', async () => {
480
+ const def = await parseWorkflowFile(pptPath);
481
+
482
+ // ① 摄取后跑确定性规划(在 design 之前,data 已齐)
483
+ const ids = def.steps.map((s) => s.id);
484
+ const am = def.steps.find((s) => s.id === 'stage_assemble_material');
485
+ expect(String(am?.args?.command)).toContain('plan_source_layout.py --build ./build');
486
+ expect(ids.indexOf('stage_assemble_material')).toBeLessThan(ids.indexOf('stage_design_doc'));
487
+
488
+ // ② 设计步「读规划、照它执行」——逐图心算被收成确定性
489
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
490
+ expect(dd).toContain('source_layout_plan'); // 读确定性规划
491
+ expect(dd).toContain('enhance_required'); // 服从「是否必须图生图」
492
+ expect(dd).toContain('enhance_prompt'); // 用算好的图生图提示词、别自己另写
493
+
494
+ // ③ 对抗评审逐张比对规划(确定性裁决)
495
+ const rv = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
496
+ expect(rv).toContain('source_layout_plan');
497
+ expect(rv).toContain('enhance_required');
498
+
499
+ // ④ 生图前确定性兜底注入:必须图生图的客户图条目 union 进 img_plan(不靠设计步记得加)
500
+ const gate = def.steps.find((s) => s.id === 'stage_img_route_gate');
501
+ expect(gate?.tool).toBe('Bash');
502
+ expect(gate?.agent).toBeUndefined();
503
+ expect(gate?.maxTurns).toBeUndefined();
504
+ expect(String(gate?.when)).toContain('design_json_ok');
505
+ const ga = String(gate?.args?.command);
506
+ expect(ga).toContain('--ensure-img-plan');
507
+ expect(ga).toContain('plan_source_layout.py');
508
+ // 2026-07-05 真机 src_9 事故:design LLM 猜错客户图扩展名(真实 .png 猜成 .jpg)致图生图找不到
509
+ // 源文件、静默退化占位渐变图,真正生成好的正确扩展名条目却晾在一边没被引用——生图前必须先对齐
510
+ // source_assets.json 真实文件名,且顺序在 --ensure-img-plan 之后(先 union 齐再纠正)。
511
+ expect(ga).toContain('--reconcile-extensions');
512
+ expect(ga.indexOf('--ensure-img-plan')).toBeLessThan(ga.indexOf('--reconcile-extensions'));
513
+ // 本轮(真机第二轮):AI 背景也确定性编进 img_plan(治「ai-* 从未进 img_plan、全退化平涂渐变」),不靠设计 LLM 记得写
514
+ expect(ga).toContain('--ensure-ai-img-plan');
515
+ // 第三轮(真机·治「route:none/html 页无底图→退化白底卡片墙」):每页背景层确定性兜底
516
+ expect(ga).toContain('--ensure-page-backgrounds');
517
+ expect(ga).toContain('image_prompt_rewrite.py');
518
+ expect(ga).toContain('image_plan_lint.py');
519
+ expect(ga).toContain('touch ./build/img_route_ok');
520
+ // when 放宽:img_plan 存在 或 规划里有待增强图 —— 表达式可被引擎求值(不抛、不畸形)
521
+ const whenExpr = String(gate?.when);
522
+ expect(whenExpr).toContain('enhance_entries');
523
+ const vars = new VarStack();
524
+ expect(() => evalExpr(whenExpr, vars)).not.toThrow();
525
+
526
+ expect(ids.indexOf('stage_gen_images')).toBeLessThan(ids.indexOf('stage_build_loop'));
527
+ expect(String(def.steps.find((s) => s.id === 'stage_build_loop')?.when)).toContain('design_json_ok');
528
+ });
529
+
530
+ it('stage_draft_from_material:以 user_request.md + material.md 为内容主干,默认不联网', async () => {
531
+ const def = await parseWorkflowFile(pptPath);
532
+ const rs = def.steps.find((s) => s.id === 'stage_draft_from_material');
533
+ const a = String(rs?.agent);
534
+ expect(a).toContain('user_request.md'); // 用户目标最高权重
535
+ expect(a).toContain('material.md'); // 以它为主干
536
+ expect(a).toContain('主干');
537
+ expect(a).toContain('高权重');
538
+ expect(a).toContain('默认**不要调用联网检索工具**');
539
+ expect(String(rs?.allowed_tools ?? '')).not.toContain('WebSearch');
540
+ expect(String(rs?.allowed_tools ?? '')).not.toContain('WebFetch');
541
+ // 图表核心结论必须落进正文(2026-07-03,治「图配了但内容对不上」)
542
+ expect(a).toContain('图表核心结论');
543
+ expect(a).toContain('必须体现为某个论点或证据');
544
+ });
545
+
546
+ it('stage_design_doc:读 user_request/material,客户图 route=source/source-enhanced 必须全用、配色随图', async () => {
547
+ const def = await parseWorkflowFile(pptPath);
548
+ const dd = def.steps.find((s) => s.id === 'stage_design_doc');
549
+ const a = String(dd?.agent);
550
+ expect(a).toContain('user_request.md'); // 回看用户目标
551
+ expect(a).toContain('material.json'); // 富 manifest
552
+ expect(a).toContain('source-image-layout.md'); // 客户图布局铁律
553
+ expect(a).toContain('route');
554
+ expect(a).toContain('source'); // route=source
555
+ expect(a).toContain('source-enhanced'); // 图生图增强客户图
556
+ expect(a).toContain('image-to-image');
557
+ expect(a).toContain('enhanced_file');
558
+ expect(a).toContain('usage_intent'); // 图片用途先决策
559
+ expect(a).toContain('enhance_required'); // 新政策:所有客户图都图生图重绘(不再按低清门槛直接用)
560
+ expect(a).toContain('contain'); // dense-info 数据图 / 盲态原图保数据才 contain
561
+ expect(a).toContain('source_file'); // design.json 字段
562
+ expect(a).toContain('orient'); // 朝向驱动构图
563
+ expect(a).toContain('vision'); // 识图详述驱动配页
564
+ expect(a).toContain('stats'); // 统计驱动配色
565
+ expect(a).toContain('mode'); // img_plan 支持 text-to-image / image-to-image
566
+ expect(a).toContain('全部'); // 硬要求:客户图必须全用
567
+ });
568
+
569
+ it('stage_design_review:审查客户图全用 + 识图对齐 + 配色协调 + 全图生图cover填满', async () => {
570
+ const def = await parseWorkflowFile(pptPath);
571
+ const rv = def.steps.find((s) => s.id === 'stage_design_review');
572
+ const a = String(rv?.agent);
573
+ expect(a).toContain('user_request.md');
574
+ expect(a).toContain('material.json');
575
+ expect(a).toContain('客户素材利用'); // 评审子项
576
+ expect(a).toContain('全部'); // 必须全部用上
577
+ expect(a).toContain('cover 填满槽'); // 新政策:source-enhanced 一律 cover 填满(dense 才 contain)
578
+ expect(a).toContain('enhance_required=true'); // 所有客户图都应图生图重绘
579
+ expect(a).toContain('source-enhanced');
580
+ expect(a).toContain('image-to-image');
581
+ expect(a).toContain('低清原图');
582
+ expect(a).toContain('vision'); // 识图对齐
583
+ expect(rv?.allowed_tools).toContain('Edit'); // 当场修订
584
+ });
585
+
586
+ it('stage_design_review:非破坏性评审,合理留白不是错误,禁止为填满新增卡片/表格/浮动面板', async () => {
587
+ const def = await parseWorkflowFile(pptPath);
588
+ const rv = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
589
+
590
+ expect(rv).toContain('非破坏性评审');
591
+ expect(rv).toContain('最高裁决顺序');
592
+ expect(rv).toContain('用户原始输入与 `build/user_request.md`');
593
+ expect(rv).toContain('Apple/DJI 式');
594
+ expect(rv).toContain('合理留白、大图背景、少量前景文案是合格目标');
595
+ expect(rv).toContain('内容预算只限制**文字过量/顶出画布**,不要求填满页面');
596
+ expect(rv).toContain('空白本身不是错误');
597
+ expect(rv).toContain('禁止为了填满页面新增卡片墙、表格墙、浮动白/灰面板');
598
+ expect(rv).toContain('禁止把影像叙事页改成文字列表页');
599
+ expect(rv).toContain('禁止把 Apple/DJI 式大图主视觉改成多块等权信息块');
600
+ expect(rv).toContain('拿不准就保留原设计');
601
+ expect(rv).toContain('不要为了填实而塞卡片/表格');
602
+ expect(rv).toContain('不要为了填满画布制造卡片');
603
+ });
604
+
605
+ it('build_batch:客户图全图生图 cover 填满 + dense contain + 按朝向/重点区 + 客户图用尽闸门', async () => {
606
+ const def = await parseWorkflowFile(pptPath);
607
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
608
+ const dos = (loop?.do ?? []) as StepDef[];
609
+ const batch = dos.find((s) => s.id === 'build_batch');
610
+ const a = String(batch?.agent);
611
+ expect(a).toContain('source-image-layout.md'); // 动手前读规则
612
+ expect(a).toContain('object-fit:contain'); // dense-info 数据图保数据才 contain
613
+ expect(a).toContain('不留白裱框'); // 新政策:cover 填满、禁白裱框/letterbox
614
+ expect(a).toContain('orient'); // 按朝向排
615
+ expect(a).toContain('src_'); // 引用客户图 src_K.png
616
+ expect(a).toContain('enhanced_file'); // route=source-enhanced 引用增强图
617
+ expect(a).toContain('data-source-file'); // 保留原图映射
618
+ expect(a).toContain('display_role'); // 用途档位决定槽位大小(background/hero/evidence/gallery)
619
+ expect(a).toContain('enhanced_file');
620
+ expect(a).toContain('回退');
621
+ expect(a).toContain('material.json'); // 取识图/统计/重点区
622
+ const probe = dos.find((s) => s.id === 'build_probe');
623
+ expect(String(probe?.args?.command)).toContain('source_usage_check.py'); // 客户图用尽确定性闸门
624
+ expect(a).toContain('key_region'); // 文字避让 / 裁切对准重点
625
+ // 默认 cover 填满槽(图生图已重绘到槽比例)
626
+ expect(a).toContain('object-fit:cover');
627
+ });
628
+
629
+ it('ppt.yaml 默认不含联网检索工具,生图支持文生图和图生图,构图门关闭占空族', async () => {
630
+ const def = await parseWorkflowFile(pptPath);
631
+ const raw = JSON.stringify(def);
632
+ expect(raw).not.toContain('WebSearch');
633
+ expect(raw).not.toContain('WebFetch');
634
+
635
+ const qualityInput = def.inputs?.find((i) => i.name === 'quality');
636
+ expect(qualityInput?.default).toBe('fast');
637
+ const debugKeepBuild = def.inputs?.find((i) => i.name === 'debug_keep_build');
638
+ expect(debugKeepBuild?.type).toBe('enum');
639
+ expect(debugKeepBuild?.values).toEqual(['true', 'false']);
640
+ expect(debugKeepBuild?.default).toBe('true');
641
+
642
+ const routeGate = def.steps.find((s) => s.id === 'stage_img_route_gate');
643
+ expect(routeGate?.tool).toBe('Bash');
644
+ expect(routeGate?.agent).toBeUndefined();
645
+ expect(routeGate?.maxTurns).toBeUndefined();
646
+ expect(String(routeGate?.args?.command)).toContain('image_prompt_rewrite.py');
647
+ expect(String(routeGate?.args?.command)).toContain('image_plan_lint.py');
648
+ // 章节错位预警(张冠李戴,2026-07-02):生图前跑 source_usage_check --design-only,
649
+ // WARNING 级恒 exit 0(|| true 双保险),绝不把模糊判定变成 halt。
650
+ expect(String(routeGate?.args?.command)).toContain('--design-only || true');
651
+
652
+ const ids = def.steps.map((s) => s.id);
653
+ expect(ids.indexOf('stage_img_route_gate')).toBeLessThan(ids.indexOf('stage_build_loop'));
654
+ expect(ids.indexOf('stage_gen_images')).toBeLessThan(ids.indexOf('stage_build_loop'));
655
+
656
+ const designReview = def.steps.find((s) => s.id === 'stage_design_review');
657
+ expect(String(designReview?.when)).toContain("inputs.quality == 'best'");
658
+
659
+ const designRepair = def.steps.find((s) => s.id === 'stage_design_repair');
660
+ expect(String(designRepair?.when)).toContain("inputs.quality == 'best'");
661
+
662
+ // 生图走专用 GenerateImage 工具(非 Bash):无 120s 帽 + 内建降级,治「Bash 半途 SIGKILL」。
663
+ const gen = def.steps.find((s) => s.id === 'stage_gen_images');
664
+ expect(gen?.agent).toBeUndefined();
665
+ expect(gen?.tool).toBe('GenerateImage');
666
+ expect(gen?.tool).not.toBe('Bash');
667
+ expect(String(gen?.args?.plan)).toBe('./build/img_plan.json');
668
+ expect(String(gen?.args?.assets)).toBe('./build/assets');
669
+ expect(String(gen?.args?.timeout_ms)).toBe('300000');
670
+ expect(String(gen?.args?.wall_budget_ms)).toBe('3600000');
671
+ expect(gen?.onError).toBe('continue');
672
+
673
+ const quality = def.steps.find((s) => s.id === 'stage_image_quality_gate');
674
+ expect(quality?.tool).toBe('GenerateImage');
675
+ expect(String(quality?.when)).toContain('image_generation_report.json');
676
+ expect(String(quality?.args?.retry_degraded_report)).toBe('./build/image_generation_report.json');
677
+ expect(String(quality?.title)).toContain('fast一轮');
678
+ expect(quality?.args?.max_rounds).toBe(1);
679
+
680
+ // 生成图语义复核(LLM 识图)已屏蔽(2026-07-02 用户裁决:验证只走确定性脚本)——
681
+ // 三步 when:"false" 整体跳过,但骨架保留(defFingerprint 不变,旧断点可续跑)。
682
+ const imageReview = def.steps.find((s) => s.id === 'stage_generated_image_review');
683
+ expect(imageReview?.type).toBe('loop');
684
+ expect(String(imageReview?.when)).toBe('false');
685
+ expect(String(imageReview?.over)).toContain('img_plan.json');
686
+ expect(String(imageReview?.over)).toContain('asArray');
687
+ const reviewSteps = (imageReview?.do ?? []) as StepDef[];
688
+ const reviewOne = reviewSteps.find((s) => s.id === 'review_generated_image');
689
+ expect(String(reviewOne?.llm)).toContain('游戏手柄');
690
+ expect(String(reviewOne?.llm)).toContain('药瓶');
691
+ expect(String(reviewOne?.llm)).toContain('普通显示器');
692
+ expect(Array.isArray(reviewOne?.images)).toBe(true);
693
+ expect(reviewOne?.history_mode).toBe('isolated');
694
+ expect(Array.isArray(reviewOne?.context_files)).toBe(true);
695
+ expect(String(reviewOne?.allowed_tools)).toContain('Read');
696
+ expect(String(reviewOne?.llm)).toContain('章节序号');
697
+ expect(String(reviewOne?.llm)).toContain('1/2/3/4');
698
+ expect(String(reviewOne?.llm)).toContain('可编辑文字');
699
+
700
+ const imageCollect = def.steps.find((s) => s.id === 'stage_generated_image_review_collect');
701
+ expect(imageCollect?.tool).toBe('Bash');
702
+ expect(String(imageCollect?.when)).toBe('false');
703
+ expect(String(imageCollect?.args?.command)).toContain('generated_image_review_collect.py');
704
+
705
+ const imageRetry = def.steps.find((s) => s.id === 'stage_generated_image_retry');
706
+ expect(imageRetry?.tool).toBe('GenerateImage');
707
+ expect(String(imageRetry?.when)).toBe('false');
708
+ expect(String(imageRetry?.args?.retry_degraded_report)).toBe('./build/generated_image_semantic_retry_report.json');
709
+ expect(String(imageRetry?.args?.max_rounds)).toBe('3');
710
+
711
+ const visualGate = def.steps.find((s) => s.id === 'stage_visual_quality_gate');
712
+ expect(visualGate?.type).toBe('loop');
713
+ // 2026-07-02 裁决:视觉修复轮 3→2,且修复步开放 fast(上一轮 fast 41 个 ERROR 零修复直接放行)。
714
+ expect(visualGate?.max_iterations).toBe(2);
715
+ const visualSteps = (visualGate?.do ?? []) as StepDef[];
716
+ const visualCheck = visualSteps.find((s) => s.id === 'visual_quality_check');
717
+ expect(visualCheck?.tool).toBe('Bash');
718
+ expect(String(visualCheck?.args?.command)).toContain('visual_quality_gate.py');
719
+ expect(String(visualCheck?.args?.command)).toContain('--strict-product-detail');
720
+ const visualRepair = visualSteps.find((s) => s.id === 'visual_repair_batch');
721
+ expect(String(visualRepair?.when)).not.toContain("inputs.quality == 'best'");
722
+ expect(String(visualRepair?.agent)).toContain('前 3–5 个问题');
723
+ expect(String(visualRepair?.agent)).toContain('卡片墙');
724
+ expect(String(visualRepair?.agent)).toContain('背景图含不可编辑文字');
725
+ expect(String(visualRepair?.agent)).toContain('紧贴文字的小背板');
726
+ expect(String(visualRepair?.agent)).toContain('deck_visual_style_check.py');
727
+ expect(String(visualRepair?.agent)).toContain('文字溢出卡片');
728
+ // 2026-07-03 版式规划执行硬门:contain 类客户图被铺整页背景(第4页影子标题事故)→
729
+ // 视觉门【版式违规】ERROR 驱动修复,修复清单必须有对应修法条目。
730
+ expect(String(visualRepair?.agent)).toContain('版式违规');
731
+ expect(String(visualRepair?.agent)).toContain('direct_contain/gallery/small_evidence');
732
+ // 2026-07-03 真机六页复盘:27 处 ERROR 里 15+ 处共享同一 VI 强调色变量,逐条加 scrim 在 2 轮预算内
733
+ // 修不完——repair 必须优先识别归组提示、一次性改共享 CSS 规则。
734
+ expect(String(visualRepair?.agent)).toContain('共享同一文字颜色');
735
+ expect(String(visualRepair?.agent)).toContain('改一次共享 CSS 规则');
736
+ // 2026-07-02 截断续跑裁决:①「修到全量零 ERROR」无界指令收敛为批内有界(撞 maxTurns 20 的直接原因);
737
+ // ② max_retries=2 启用引擎 maxTurns 截断续作(同 history 续跑,每次再给 20 轮),0 会把「可续跑」废掉。
738
+ expect(String(visualRepair?.agent)).not.toContain('只要还有 ERROR');
739
+ expect(String(visualRepair?.agent)).toContain('留给外层循环下一轮');
740
+ expect(visualRepair?.max_retries).toBe(2);
741
+ expect(visualRepair?.onError).toBe('continue');
742
+
743
+ const exp = def.steps.find((s) => s.id === 'stage_export');
744
+ expect(exp?.tool).toBe('Bash');
745
+ expect(exp?.agent).toBeUndefined();
746
+ expect(String(exp?.args?.command)).toContain('--no-occupancy');
747
+ expect(String(exp?.args?.command)).toContain('layout_balance: REPORT_ONLY');
748
+ expect(String(exp?.args?.command)).toContain('--require-existing-derived');
749
+ // 确定性兜底:缺失的 enhanced 派生图从原图拷顶上(防 deck 空框/死链)
750
+ expect(String(exp?.args?.command)).toContain('--reconcile-missing');
751
+ expect(String(exp?.args?.command)).toContain('source_usage_check.py');
752
+ // 减法(2026-07-02):stage_export 不再重跑 deck_visual_style_check(与阶段18 视觉门
753
+ // 完全同参双跑、且视觉门达轮数已带残留放行)——style 检查唯一跑点在
754
+ // stage_visual_quality_gate(fast/best 统一 2 轮修复),导出只验哨兵。
755
+ expect(String(exp?.args?.command)).not.toContain('deck_visual_style_check.py');
756
+ expect(String(exp?.args?.command)).toContain('test -f ./build/visual_quality_ok');
757
+ expect(exp?.done_when).toBeUndefined();
758
+
759
+ const design = def.steps.find((s) => s.id === 'stage_design_doc');
760
+ const da = String(design?.agent);
761
+ expect(da).toContain('安装演示');
762
+ expect(da).toContain('产品外观');
763
+ expect(da).toContain('真实使用情境');
764
+ expect(da).toContain('柱状图');
765
+ expect(da).toContain('折线图');
766
+ expect(da).toContain('HTML/CSS/SVG');
767
+ });
768
+
769
+ it('内容预算确定性化 + 配图占满槽位 + 插图图层:治顶出/躲角/被遮挡', async () => {
770
+ const def = await parseWorkflowFile(pptPath);
771
+
772
+ // ① 设计评审前后都有确定性脚本检查;评审 agent 只读报告短修订,不再自己跑 Bash。
773
+ const precheck = def.steps.find((s) => s.id === 'stage_design_json_gate');
774
+ expect(precheck?.tool).toBe('Bash');
775
+ const pa = String(precheck?.args?.command);
776
+ expect(pa).toContain('normalize_design_json.py');
777
+ expect(pa).toContain('design_json_ok');
778
+ expect(pa).toContain('design_budget_check.py');
779
+ expect(pa).toContain('design_style_check.py');
780
+ expect(pa).toContain('design_review_checks.md');
781
+ expect(pa).toContain('design_review_checks.json');
782
+
783
+ const review = def.steps.find((s) => s.id === 'stage_design_review');
784
+ expect(String(review?.when)).toContain("inputs.quality == 'best'");
785
+ expect(review?.allowed_tools ?? []).not.toContain('Bash');
786
+ const ra = String(review?.agent);
787
+ expect(ra).toContain('design_review_checks.md');
788
+ expect(ra).toContain('只读预检报告');
789
+ expect(ra).toContain('不再自己跑脚本');
790
+
791
+ const recheck = def.steps.find((s) => s.id === 'stage_design_recheck');
792
+ expect(recheck?.tool).toBe('Bash');
793
+ const rca = String(recheck?.args?.command);
794
+ expect(rca).toContain("inputs.quality");
795
+ expect(rca).toContain('DESIGN_REVIEW_RECHECK_SKIPPED_FAST');
796
+ expect(rca).toContain('design_review_checks.json');
797
+ expect(rca).toContain('design_repair_residual.md');
798
+ expect(rca).toContain('design_budget_check.py');
799
+ expect(rca).toContain('design_style_check.py');
800
+ expect(rca).toContain('design_review_recheck.md');
801
+ expect(rca).toContain('design_review_recheck.json');
802
+
803
+ const repair = def.steps.find((s) => s.id === 'stage_design_repair');
804
+ expect(String(repair?.when)).toContain("inputs.quality == 'best'");
805
+ expect(String(repair?.when)).toContain('design_review_recheck.json');
806
+ expect(String(repair?.when)).toContain("'ok'");
807
+ expect(repair?.type).toBe('loop');
808
+ expect(repair?.max_iterations).toBe(3);
809
+ const repairSteps = (repair?.do ?? []) as StepDef[];
810
+ expect(String(repairSteps.find((s) => s.id === 'design_repair_hints')?.args?.command)).toContain('design_repair_hints.py');
811
+ const repairBatch = repairSteps.find((s) => s.id === 'design_repair_batch');
812
+ expect(String(repairBatch?.when)).toContain("inputs.quality == 'best'");
813
+ expect(String(repairBatch?.agent)).toContain('本批页面短修复');
814
+
815
+ // ② 设计步:预算会被确定性核对 + 目录/list 超预算默认双列 + source-enhanced aspect=槽位
816
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
817
+ expect(dd).toContain('design_budget_check.py'); // 第一版就按预算设计
818
+ expect(dd).toContain('双列');
819
+ expect(dd).toContain('槽位的比例'); // route=source-enhanced 的 aspect = 槽位比例(图生图重绘到槽位)
820
+
821
+ // ③ 构建步:图片必须填满槽位(治躲角/半填)+ 插图次顶层不被非文本遮挡
822
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
823
+ expect(loop?.max_iterations).toBeLessThanOrEqual(6);
824
+ const dos = (loop?.do ?? []) as StepDef[];
825
+ const probe = dos.find((s) => s.id === 'build_probe');
826
+ expect(probe?.tool).toBe('Bash');
827
+ expect(String(probe?.args?.command)).toContain('deck_pages_check.py');
828
+ expect(String(probe?.args?.command)).toContain('source_usage_check.py');
829
+ expect(String(probe?.args?.command)).toContain('build_probe.json');
830
+ expect(String(probe?.args?.command)).toContain('build_complete');
831
+ const batchStep = dos.find((s) => s.id === 'build_batch');
832
+ expect(String(batchStep?.when)).toContain("!fileExists('./build/build_complete')");
833
+ expect(batchStep?.maxTurns).toBeLessThanOrEqual(24);
834
+ expect(batchStep?.max_retries).toBe(1);
835
+ const batch = String(batchStep?.agent);
836
+ expect(batch).toContain('build_probe.txt');
837
+ expect(batch).toContain('填满该单元'); // 占满槽位铁律
838
+ expect(batch).toContain('object-fit:contain');
839
+ expect(batch).toContain('object-position'); // cover 时按 key_region 填满裁边
840
+ expect(batch).toContain('次顶层'); // 插图图层:文本之下、背景之上
841
+ expect(batch).toContain('遮挡');
842
+ const done = dos.find((s) => s.id === 'build_done');
843
+ expect(String(done?.when)).toContain("fileExists('./build/build_complete')");
844
+ });
845
+
846
+ it('fast 链路清理旧运行哨兵,且说明/报告顺序为先生图后构建', async () => {
847
+ const def = await parseWorkflowFile(pptPath);
848
+ const desc = String(def.description);
849
+ expect(desc.indexOf('配图分流')).toBeLessThan(desc.indexOf('按设计文档**分批生成** HTML deck'));
850
+ expect(desc).toContain('默认 fast 每轮 ≤3 页');
851
+
852
+ const extract = def.steps.find((s) => s.id === 'stage_extract');
853
+ const cmd = String(extract?.args?.command);
854
+ for (const marker of [
855
+ 'build_complete',
856
+ 'layout_ok',
857
+ 'visual_quality_ok',
858
+ 'visual_quality_state.json',
859
+ 'image_generation_report.json',
860
+ 'generated_image_review.json',
861
+ 'design_json_ok',
862
+ 'design_json_gate.json',
863
+ 'design.raw.json',
864
+ 'export-summary.md',
865
+ 'build_probe.json',
866
+ ]) {
867
+ expect(cmd).toContain(marker);
868
+ }
869
+ expect(cmd).toContain('rm -rf ./build/export ./build/generated-image-reviews');
870
+
871
+ const cleanup = def.steps.find((s) => s.id === 'cleanup_intermediates');
872
+ const cleanupCmd = String(cleanup?.args?.command);
873
+ expect(cleanupCmd).toContain('inputs.debug_keep_build');
874
+ expect(cleanupCmd).toContain('rm -rf ./build');
875
+ expect(cleanupCmd).toContain('debug_keep_build=true');
876
+ expect(cleanupCmd).not.toContain('[TEMP]');
877
+ expect(cleanupCmd).not.toContain('上线前');
878
+
879
+ const report = String(def.steps.find((s) => s.id === 'final_report')?.message);
880
+ expect(report.indexOf('配图分流')).toBeLessThan(report.indexOf('分批生成网页'));
881
+ expect(report).toContain('产品详情页视觉硬门');
882
+ expect(report).toContain('debug_keep_build=${inputs.debug_keep_build}');
883
+ expect(report).not.toContain('已在收尾清理——只保留最终 PPT');
884
+ });
885
+
886
+ it('标题跨页对齐约定 + 插图忠实落槽 + 标题长度(治导入PPT不齐/位置不对/超界,均 web 端)', async () => {
887
+ const def = await parseWorkflowFile(pptPath);
888
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
889
+ const dos = (loop?.do ?? []) as StepDef[];
890
+ const batch = String(dos.find((s) => s.id === 'build_batch')?.agent);
891
+
892
+ // ① 统一 deck 头部约定:主标/副标同左对齐、跨页一致(治标题导入 PPT 不对齐)
893
+ expect(batch).toContain('deck-header');
894
+ expect(batch).toContain('deck-title');
895
+ expect(batch).toContain('deck-subtitle');
896
+ expect(batch).toContain('跨页对齐');
897
+
898
+ // ② 插图位置忠实于 design 指定槽位(治「位置不是设计的」)
899
+ expect(batch).toContain('design 指定的槽位');
900
+ expect(batch).toContain('位置忠实');
901
+
902
+ // ③ 设计步:内容页标题 ≤14 字 + 跨页对齐
903
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
904
+ expect(dd).toContain('≤14 字');
905
+ expect(dd).toContain('左对齐');
906
+ });
907
+
908
+ it('真机复盘重构:内容内边距不贴边 + 客户图≤1×不缩小 + 封面AI背景必出 + 卡内文字禁溢出 + 对抗评审质问式自证', async () => {
909
+ const def = await parseWorkflowFile(pptPath);
910
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
911
+ const dos = (loop?.do ?? []) as StepDef[];
912
+ const batch = String(dos.find((s) => s.id === 'build_batch')?.agent);
913
+
914
+ // ① 统一内容内边距、标题不贴画布边;上下改 56/48 少吃竖向(治标题贴最左 + 给内容更多高度)
915
+ expect(batch).toContain('内容内边距');
916
+ expect(batch).toContain('padding:56px 112px 48px');
917
+ expect(batch).toContain('不贴画布边');
918
+
919
+ // ①b 结构性高度 bound(治反复出现的「内容超出下边缘」——slide 物理上溢不出)
920
+ expect(batch).toContain('结构性高度 bound');
921
+ expect(batch).toContain('min-height:0');
922
+ expect(batch).toContain('max-height:100%');
923
+ expect(batch).toContain('grid-template-rows:1fr 1fr');
924
+ // ①b-2 2026-07-05 真机复盘:grid-row 图片用 aspect-ratio 独立撑高、压穿下一行(第4页93%重叠事故)
925
+ expect(batch).toContain('aspect-ratio:21/9');
926
+ expect(batch).toContain('height:100%; min-height:0');
927
+
928
+ // ①c 背景层 = 逐字复用锁定骨架,🚫绝不渐变压图(治真机头号事故:兜底渐变盖死 AI 图)
929
+ expect(batch).toContain('绝不渐变压图');
930
+ expect(batch).toContain('锁定编辑式三层骨架');
931
+ expect(batch).toContain('--vi-fallback'); // 纯色兜底在底,图 cover 填满、不被渐变盖
932
+ expect(batch).toContain('禁自创'); // 禁自创无 grid 定义的 bespoke 布局类(治塌成空白页)
933
+
934
+ // ② 客户图尺寸/缩放铁律:display_role + ≤1× 不放大糊 + 最小占比不缩成角落小图
935
+ expect(batch).toContain('display_role');
936
+ expect(batch).toContain('max_upscale');
937
+ expect(batch).toContain('≤1×');
938
+ expect(batch).toContain('min_canvas_fraction');
939
+ expect(batch).toContain('绝不放大超过原生像素');
940
+
941
+ // ③ 卡内文字禁溢出 + 一行卡数随文长(治第 5/11/12 页窄卡长文重叠)
942
+ expect(batch).toContain('不溢出');
943
+ expect(batch).toContain('一行最多 3 卡');
944
+
945
+ // ④ design_doc:封面/章节/尾页 AI 背景从「可用」改「必出」(治封面空洞无背景)
946
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
947
+ expect(dd).toContain('必出');
948
+
949
+ // ⑤ ⭐ 对抗评审 = 质问式自证(预设有罪、逐条给证据、别自我表扬)—— 用户核心补充
950
+ const rv = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
951
+ expect(rv).toContain('预设你有罪');
952
+ expect(rv).toContain('用事实自证');
953
+ expect(rv).toContain('自我表扬');
954
+ expect(rv).toContain('罪状');
955
+ // 铁律二:证据门控编辑——治「胆小 AI 把对的改错」过度修正(唯证据坐实才改、量不准默认保留)
956
+ expect(rv).toContain('把对的改错');
957
+ expect(rv).toContain('无依据的改动比漏判更糟');
958
+ expect(rv).toContain('证据坐实违规');
959
+ expect(rv).toContain('默认保留不动');
960
+ // 质问清单覆盖累积真实失败模式:标题贴左 / 客户图缩放 / 文字溢出重叠
961
+ expect(rv).toContain('贴到画布最左');
962
+ expect(rv).toContain('display_role');
963
+ expect(rv).toContain('重叠糊成一团');
964
+ });
965
+
966
+ it('编辑式融合转向:图做背景文字浮前景设为默认 + 出血背景分层 + padding 只束前景(治卡片墙主导、不像产品详情页)', async () => {
967
+ const def = await parseWorkflowFile(pptPath);
968
+
969
+ // ① design_doc:先定整页视觉策略、编辑式优先;引用 §2.2 产品详情页式融合库;点名「不靠表格/网格/悬浮卡片划分区域」
970
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
971
+ expect(dd).toContain('先定整页视觉策略');
972
+ expect(dd).toContain('§2.2 模板库'); // 引用编辑式模板库(§0 最高标准 + §2.2)
973
+ expect(dd).toContain('背景主导');
974
+ expect(dd).toContain('不靠表格/网格/悬浮卡片划分区域');
975
+
976
+ // ②(关键)编辑式融合页不算稀疏页、不受 ≤20% 硬帽——治「80% 被逼成卡片填满页」
977
+ expect(dd).toContain('编辑式融合页 ≠ 稀疏页');
978
+ expect(dd).toContain('不受下面 20% 限额约束');
979
+
980
+ // ③ render 决策重平衡:html+text 卡片不再是自动默认;产品/场景/能力优先图背景+前景文字
981
+ expect(dd).toContain('卡片不再是自动默认');
982
+ expect(dd).toContain('图作背景或半幅 hero、文字作前景浮层');
983
+
984
+ // ④ build_batch:编辑式三层分离(出血背景层 + scrim + 前景守边),内容页也可用
985
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
986
+ const dos = (loop?.do ?? []) as StepDef[];
987
+ const batch = String(dos.find((s) => s.id === 'build_batch')?.agent);
988
+ expect(batch).toContain('编辑式融合分层');
989
+ expect(batch).toContain('edit-bg');
990
+ expect(batch).toContain('inset:0'); // 背景层出血、被 slide overflow:hidden 钳死
991
+ expect(batch).toContain('出血到画布边');
992
+ expect(batch).toContain('edit-scrim');
993
+ expect(batch).toContain('edit-content');
994
+ // 本轮:前景层禁卡墙网格(原「别把编辑式页拆成一堆悬浮卡片」强化为明确枚举 + 禁令)
995
+ expect(batch).toContain('禁卡片墙、禁网格划区、禁表格');
996
+ // 本轮(真机第二轮·治头号事故):archetype 栅格落 .edit-content、绝不写在 .slide(否则两栏塌进左半、第二栏溢出被裁)
997
+ expect(batch).toContain('塌进左半');
998
+ expect(batch).toContain('.slide.X .edit-content');
999
+ // AI 底图按约定名 ai_p<page>.png 引用 + onerror 渐变兜底(内容页绝不只上 CSS 渐变 class 不放 img)
1000
+ expect(batch).toContain('ai_p<page>.png');
1001
+ expect(batch).toContain('ai_p<page>_fg.png');
1002
+ expect(batch).toContain('背景专用');
1003
+ expect(batch).toContain('ai-fallback');
1004
+ // 图槽不再裱白卡/选项卡:图要么做整页底,要么 cover 满半幅,没有「小图浮白卡」中间态
1005
+ expect(batch).toContain('没有「小图浮白卡」这种中间态');
1006
+
1007
+ // 第三轮(真机·治「半数内容页无背景→卡墙」):背景层恒在、route 只决定前景;卡墙是没背景的下游症状
1008
+ expect(batch).toContain('每个内容页 `.edit-bg` 必有背景图层');
1009
+ expect(batch).toContain('`route` 只决定【前景怎么呈现】');
1010
+ expect(batch).toContain('因为 route=html/none 就不给背景层'); // 明令禁止
1011
+ // 现在每页都是编辑式三层页 → 「前景禁卡墙」对所有内容页生效(含 route=html/none 结构页)
1012
+ expect(batch).toContain('每个内容页都有背景层、都是编辑式三层页');
1013
+ expect(batch).toContain('磨砂文字垫'); // 只作文字背板、绝不是分区卡片
1014
+ expect(batch).toContain('绝不是分区卡片');
1015
+ // 透明抠图合流:cutout = 透明前景特写插图,绝不套白框(与去框铁律一致)
1016
+ expect(batch).toContain('enhance_style=cutout');
1017
+ expect(batch).toContain('透明特写插图');
1018
+ expect(batch).toContain('background:transparent');
1019
+ expect(batch).toContain('绝不套 `.src-figure` 白底/边框/padding');
1020
+
1021
+ // 本轮:统一字体 token + 结构图连线归属,防最后几页小字和孤立线条
1022
+ expect(batch).toContain('--fs-hero');
1023
+ expect(batch).toContain('--fs-axis');
1024
+ expect(batch).toContain('字体 token');
1025
+ expect(batch).toContain('diagram-node');
1026
+ expect(batch).toContain('diagram-link');
1027
+ expect(batch).toContain('diagram-label');
1028
+
1029
+ // ⑤(关键和谐)padding 只束前景文字层、背景层例外出血——不退化前轮「贴边/超下边缘」修复
1030
+ expect(batch).toContain('图出血、字守边');
1031
+ expect(batch).toContain('只针对前景内容/文字层');
1032
+ // 前轮溢出修复断言仍在(共存、不回归)
1033
+ expect(batch).toContain('padding:56px 112px 48px');
1034
+ expect(batch).toContain('min-height:0');
1035
+
1036
+ // ⑥ 本轮(第二轮深化):有底图→禁表格卡墙硬条件 + 表格降级图表优先 + 过载页混合处理
1037
+ expect(dd).toContain('有底图就别再划区');
1038
+ expect(dd).toContain('禁用表格');
1039
+ expect(dd).toContain('禁用悬浮卡片墙');
1040
+ expect(dd).toContain('字色随底图明暗自适应');
1041
+ expect(dd).toContain('过载页按内容分流');
1042
+ expect(dd).toContain('整合为一张 hero');
1043
+ expect(dd).toContain('优先图表化'); // 数据/对比优先图表而非表格
1044
+ expect(dd).toContain('表格是最后手段');
1045
+
1046
+ // ⑦ design_review 加罪状 🅵:有底图页是否仍表格/卡墙 + 过载页是否处理(服从 layout_constraint)
1047
+ const review = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
1048
+ expect(review).toContain('🅵');
1049
+ expect(review).toContain('有底图还在表格/卡墙划区');
1050
+ expect(review).toContain('layout_constraint=fusion_no_cards');
1051
+ });
1052
+
1053
+ it('产品详情页式硬字段:design.json 必须驱动构图而不是只靠 archetype/composition', async () => {
1054
+ const def = await parseWorkflowFile(pptPath);
1055
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
1056
+ const review = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
1057
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
1058
+ const batch = String(((loop?.do ?? []) as StepDef[]).find((s) => s.id === 'build_batch')?.agent);
1059
+
1060
+ expect(dd).toContain('composition_style');
1061
+ expect(dd).toContain('page_grammar');
1062
+ expect(dd).toContain('one_sentence_takeaway');
1063
+ expect(dd).toContain('visual_focus');
1064
+ expect(dd).toContain('text_zone');
1065
+ expect(dd).toContain('safe_text_zone');
1066
+ expect(dd).toContain('visual_role');
1067
+ expect(dd).toContain('primary_visual_type');
1068
+ expect(dd).toContain('image_weight');
1069
+ expect(dd).toContain('text_overlay_mode');
1070
+ expect(dd).toContain('max_visible_words');
1071
+ expect(dd).toContain('copy_budget');
1072
+ expect(dd).toContain('main_visual_layer');
1073
+ expect(dd).toContain('supporting_visual_layer');
1074
+ expect(dd).toContain('text_overlay_zone');
1075
+ expect(dd).toContain('visual_story');
1076
+ expect(dd).toContain('semantic_image_prompt');
1077
+ expect(dd).toContain('forbidden_layout');
1078
+ expect(dd).toContain('visual_asset_contract');
1079
+ expect(dd).toContain('背景和前景**不得使用同一张图片**');
1080
+ expect(dd).toContain('ai_p<page>_fg.png');
1081
+ expect(dd).toContain('背景/场景/hero 图零文字铁律');
1082
+ expect(dd).toContain('章节序号');
1083
+ expect(dd).toContain('HTML/SVG 可编辑前景层');
1084
+ // 章节锚定配页(治张冠李戴,2026-07-02):客户图按来源章节 section 锚定配页,语义契合只用于章节内挑页。
1085
+ expect(dd).toContain('章节锚定配页');
1086
+ expect(dd).toContain('张冠李戴');
1087
+ expect(dd).toContain('图表核心结论必须呼应'); // stage_design_doc 自身的配图规则(2026-07-03)
1088
+ // 图表结论呼应罪状(2026-07-03):key_message 必须体现在该页 key_point/evidence 里,在 review 罪状清单。
1089
+ expect(review).toContain('图表结论呼应核对');
1090
+ expect(review).toContain('key_message');
1091
+ // 原图文字重配(2026-07-03 src_6 事故:带字图 img2img 去字后,字的内容必须能在设计阶段判断是否重配)。
1092
+ expect(dd).toContain('原图文字重配');
1093
+ expect(dd).toContain('embedded_text');
1094
+ expect(review).toContain('原图文字重配核对');
1095
+ // 卡片自适应(末页 CTA 字骑出卡缘事故):build_batch 禁固定宽高卡片 + 更宽文字/wrapper。
1096
+ expect(batch).toContain('文字溢出卡片');
1097
+ expect(batch).toContain('卡尺寸随内容自然撑开');
1098
+ expect(dd).toContain('product-hero|feature-theater|detail-closeup|scenario-fullbleed|data-visual|gallery-strip|chapter-cinematic|quote-focus');
1099
+ expect(dd).toContain('hero-scene|cinematic-feature|product-closeup|image-mosaic|spec-overlay|data-visual|before-after-scene');
1100
+ expect(dd).toContain('DJI 式网格');
1101
+ expect(dd).toContain('不要只靠 `archetype/composition`');
1102
+
1103
+ const precheck = String(def.steps.find((s) => s.id === 'stage_design_json_gate')?.args?.command);
1104
+ const recheck = String(def.steps.find((s) => s.id === 'stage_design_recheck')?.args?.command);
1105
+ expect(precheck).toContain('normalize_design_json.py');
1106
+ expect(precheck).toContain('design_json_ok');
1107
+ expect(precheck).toContain('design_style_check.py');
1108
+ expect(precheck).toContain('DESIGN_REVIEW_PRECHECK_OK');
1109
+ expect(recheck).toContain('normalize_design_json.py');
1110
+ expect(recheck).toContain('design_style_check.py');
1111
+ expect(recheck).toContain('DESIGN_REVIEW_RECHECK_OK');
1112
+ expect(review).toContain('design_review_checks.md');
1113
+
1114
+ expect(batch).toContain('composition_style');
1115
+ expect(batch).toContain('page_grammar');
1116
+ expect(batch).toContain('image_weight');
1117
+ expect(batch).toContain('main_visual_layer');
1118
+ expect(batch).toContain('semantic_image_prompt');
1119
+ expect(batch).toContain('forbidden_layout');
1120
+ expect(batch).toContain('image-mosaic');
1121
+ expect(batch).toContain('最多 5 页'); // 2026-07-02 提速:fast 每批 3→5 页
1122
+ expect(batch).toContain('data-visual');
1123
+ expect(batch).toContain('禁止大表格矩阵');
1124
+ const visualGate = def.steps.find((s) => s.id === 'stage_visual_quality_gate');
1125
+ const visualSteps = (visualGate?.do ?? []) as StepDef[];
1126
+ expect(String(visualSteps.find((s) => s.id === 'visual_quality_check')?.args?.command)).toContain('visual_quality_gate.py');
1127
+ });
1128
+
1129
+ it('编辑式最高标准·强制默认(第三轮·下沉到模板层):每页背景层 + cards 罕见例外', async () => {
1130
+ const def = await parseWorkflowFile(pptPath);
1131
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
1132
+ // 编辑式从「优先」升级为「强制默认」+ 每个内容页都有背景层(治无客户图页退回卡墙)
1133
+ expect(dd).toContain('编辑式最高标准·强制默认');
1134
+ expect(dd).toContain('每个内容页都必须有背景层');
1135
+ expect(dd).toContain('AI 全幅底图'); // 无客户图时质量优先,渐变只作失败兜底
1136
+ expect(dd).toContain('卡片墙 / 网格 / 表格 = 罕见例外');
1137
+ // archetype 行标注 cards 降为罕见例外
1138
+ expect(dd).toContain('`cards` 已降为罕见例外、默认别选');
1139
+ // image_intensity 每页背景层底线
1140
+ expect(dd).toContain('每个内容页都必须有背景层(★编辑式最高标准·治「白底卡海」)');
1141
+ // 第三轮(真机·治「route:none 当『这页不要图』→白底卡墙」):心智模型 + 内容页避免 route:none
1142
+ expect(dd).toContain('背景层【永远有】');
1143
+ expect(dd).toContain('【不决定有没有背景】');
1144
+ expect(dd).toContain('内容页避免 `route:none`');
1145
+ expect(dd).toContain('--ensure-page-backgrounds'); // 留空也会被确定性补氛围底图
1146
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
1147
+ const buildBatch = String(((loop?.do ?? []) as StepDef[]).find((s) => s.id === 'build_batch')?.agent);
1148
+ expect(buildBatch).toContain('所有 SVG 连线必须有语义归属');
1149
+ expect(buildBatch).toContain('source-enhanced 图层必须服从');
1150
+ });
1151
+
1152
+ it('第五轮·结构性根治:锁定骨架/AI背景默认/苹果式精炼/画像/size2k', async () => {
1153
+ const def = await parseWorkflowFile(pptPath);
1154
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
1155
+ // 动笔前三问 + 苹果式精辟文案 + 识图×文案微调
1156
+ expect(dd).toContain('动笔前三问');
1157
+ expect(dd).toContain('MacBook Pro');
1158
+ expect(dd).toContain('术语深度档');
1159
+ expect(dd).toContain('识图×文案对应');
1160
+ // AI 背景默认、渐变只作失败兜底(反转上轮 VI 渐变默认)
1161
+ expect(dd).toContain('默认高清 AI 图,渐变只作失败兜底');
1162
+ expect(dd).toContain('优先配高清 AI 全幅底图');
1163
+ // stage0 画像参数化 + draft 按画像调深度
1164
+ const s0 = String(def.steps.find((s) => s.id === 'stage0_clarify')?.agent);
1165
+ expect(s0).toContain('术语深度档');
1166
+ const draft = String(def.steps.find((s) => s.id === 'stage_draft_from_material')?.agent);
1167
+ expect(draft).toContain('术语深度档');
1168
+ // review 渐变压图罪状
1169
+ const review = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
1170
+ expect(review).toContain('绝不渐变压图');
1171
+ // 生图分辨率 2k(成本不计、高清)
1172
+ const gen = def.steps.find((s) => s.id === 'stage_gen_images');
1173
+ expect(String((gen?.args as Record<string, unknown>)?.size)).toBe('2k');
1174
+ });
1175
+
1176
+ it('视觉优先重构:讲稿通道 speaker_notes + 一页一想法 + 朗读测试(治文本阅读型页面)', async () => {
1177
+ const def = await parseWorkflowFile(pptPath);
1178
+ const dd = String(def.steps.find((s) => s.id === 'stage_design_doc')?.agent);
1179
+ // 演讲式内容契约:屏上短语化、论证进讲稿(导出进 PPT 备注栏)、一页一想法、3–5 秒朗读测试
1180
+ expect(dd).toContain('speaker_notes');
1181
+ expect(dd).toContain('一页 = 一个想法');
1182
+ expect(dd).toContain('朗读测试');
1183
+ expect(dd).toContain('evidence 是图表与讲稿的原料');
1184
+ // review 罪状 🅸 文章化页面(量证据自证,内容不删换地方)
1185
+ const review = String(def.steps.find((s) => s.id === 'stage_design_review')?.agent);
1186
+ expect(review).toContain('文章化页面');
1187
+ expect(review).toContain('speaker_notes');
1188
+ // build_batch:讲稿落盘为 hidden aside(不渲染不计上屏字数,导出器写进备注栏)+ 上屏禁扩写
1189
+ const loop = def.steps.find((s) => s.id === 'stage_build_loop');
1190
+ const batch = String(((loop?.do ?? []) as StepDef[]).find((s) => s.id === 'build_batch')?.agent);
1191
+ expect(batch).toContain('speaker-notes');
1192
+ expect(batch).toContain('讲稿落盘');
1193
+ expect(batch).toContain('禁扩写');
1194
+ });
1195
+
1196
+ it('所有 ${} 表达式都能被引擎解析(不含畸形语法)', async () => {
1197
+ const def = await parseWorkflowFile(pptPath);
1198
+ const vars = new VarStack();
1199
+ const inputsObj: Record<string, unknown> = {};
1200
+ for (const inp of def.inputs ?? []) inputsObj[inp.name] = inp.default ?? '';
1201
+ vars.setGlobal('inputs', inputsObj);
1202
+ // 循环变量 img(vision_loop 的 as),给个占位对象让 ${img.file} 可解析
1203
+ vars.set('img', { file: 'src_1.png', orient: 'landscape', section: 's' });
1204
+ vars.set('clarifications', '采用测试风格,面向管理层。');
1205
+ vars.set('vision_json', '{"description":"测试图片"}');
1206
+
1207
+ const strings: string[] = [];
1208
+ const walk = (v: unknown): void => {
1209
+ if (typeof v === 'string') strings.push(v);
1210
+ else if (Array.isArray(v)) for (const x of v) walk(x);
1211
+ else if (v && typeof v === 'object')
1212
+ for (const x of Object.values(v as Record<string, unknown>)) walk(x);
1213
+ };
1214
+ walk(def);
1215
+ for (const s of strings) {
1216
+ if (!s.includes('${')) continue;
1217
+ expect(() => interpolate(s, vars)).not.toThrow();
1218
+ }
1219
+ });
1220
+ });