mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,1966 @@
1
+ import { reviewOfficeStructure } from './assurance.mjs';
2
+ import {
3
+ bindOfficeContent,
4
+ normalizeOfficeContentModel,
5
+ summarizeOfficeContentModel,
6
+ } from './content-model.mjs';
7
+
8
+ const FORMATS = new Set(['docx', 'xlsx', 'pptx', 'pdf', 'csv', 'tsv']);
9
+ const PPTX_BACKGROUND_MODES = new Set(['sandwich', 'light', 'dark', 'custom']);
10
+ const PPTX_TEMPLATE_MODES = new Set(['prefer', 'strict', 'scratch']);
11
+ const PPTX_VISUAL_TYPES = new Set(['statement', 'typography', 'comparison', 'process', 'metrics', 'chart', 'table', 'image', 'diagram', 'matrix']);
12
+ const PPTX_FOCAL_REGIONS = new Set(['full', 'left', 'right', 'center', 'top', 'bottom']);
13
+ const PPTX_CRITIQUE_AXES = Object.freeze(['hierarchy', 'balance', 'legibility', 'cohesion', 'evidence']);
14
+
15
+ const DESIGN_PACKS = Object.freeze({
16
+ editorial: Object.freeze({
17
+ id: 'editorial',
18
+ label: 'Editorial',
19
+ bestFor: 'Reports, proposals, briefs, and narrative presentations.',
20
+ tokens: Object.freeze({
21
+ colors: Object.freeze({
22
+ canvas: 'FFFFFF',
23
+ ink: '171717',
24
+ muted: '5F6368',
25
+ accent: 'C43E2F',
26
+ accent2: 'D89B2B',
27
+ surface: 'F1F2F4',
28
+ surface2: 'E7E9EC',
29
+ inverse: '151515',
30
+ onAccent: 'FFFFFF',
31
+ onInverse: 'FFFFFF',
32
+ }),
33
+ typography: Object.freeze({
34
+ display: 'Segoe UI',
35
+ body: 'Segoe UI',
36
+ data: 'Segoe UI',
37
+ }),
38
+ spacing: Object.freeze({
39
+ compact: 12,
40
+ block: 24,
41
+ section: 42,
42
+ page: 54,
43
+ }),
44
+ }),
45
+ formats: Object.freeze({
46
+ pptx: Object.freeze({ title: 40, body: 18, caption: 12, canvasWidth: 960, canvasHeight: 540 }),
47
+ docx: Object.freeze({ title: 28, heading1: 18, heading2: 14, body: 10.5, margin: 64.8 }),
48
+ xlsx: Object.freeze({ title: 18, heading: 11, body: 10, tableStyle: 'TableStyleMedium2' }),
49
+ pdf: Object.freeze({ title: 28, heading: 18, body: 10.5, margin: 54 }),
50
+ }),
51
+ }),
52
+ technical: Object.freeze({
53
+ id: 'technical',
54
+ label: 'Technical',
55
+ bestFor: 'Product, engineering, architecture, and operational material.',
56
+ tokens: Object.freeze({
57
+ colors: Object.freeze({
58
+ canvas: 'F7FAFC',
59
+ ink: '102A43',
60
+ muted: '627D98',
61
+ accent: '008C8C',
62
+ accent2: 'E0A526',
63
+ surface: 'E8F1F5',
64
+ surface2: 'D9E7EC',
65
+ inverse: '0B1F33',
66
+ onAccent: 'FFFFFF',
67
+ onInverse: 'F7FAFC',
68
+ }),
69
+ typography: Object.freeze({
70
+ display: 'Segoe UI',
71
+ body: 'Segoe UI',
72
+ data: 'Cascadia Mono',
73
+ }),
74
+ spacing: Object.freeze({
75
+ compact: 10,
76
+ block: 22,
77
+ section: 38,
78
+ page: 50,
79
+ }),
80
+ }),
81
+ formats: Object.freeze({
82
+ pptx: Object.freeze({ title: 38, body: 17, caption: 12, canvasWidth: 960, canvasHeight: 540 }),
83
+ docx: Object.freeze({ title: 26, heading1: 17, heading2: 13, body: 10.5, margin: 61.2 }),
84
+ xlsx: Object.freeze({ title: 18, heading: 11, body: 10, tableStyle: 'TableStyleMedium4' }),
85
+ pdf: Object.freeze({ title: 27, heading: 17, body: 10.5, margin: 52 }),
86
+ }),
87
+ }),
88
+ data: Object.freeze({
89
+ id: 'data',
90
+ label: 'Data',
91
+ bestFor: 'Financial models, analytical reports, dashboards, and KPI reviews.',
92
+ tokens: Object.freeze({
93
+ colors: Object.freeze({
94
+ canvas: 'FFFFFF',
95
+ ink: '1F2933',
96
+ muted: '66788A',
97
+ accent: '287A4B',
98
+ accent2: 'D97706',
99
+ surface: 'EDF5F0',
100
+ surface2: 'E3EAF0',
101
+ inverse: '183028',
102
+ onAccent: 'FFFFFF',
103
+ onInverse: 'FFFFFF',
104
+ }),
105
+ typography: Object.freeze({
106
+ display: 'Segoe UI',
107
+ body: 'Segoe UI',
108
+ data: 'Segoe UI',
109
+ }),
110
+ spacing: Object.freeze({
111
+ compact: 9,
112
+ block: 20,
113
+ section: 36,
114
+ page: 48,
115
+ }),
116
+ }),
117
+ formats: Object.freeze({
118
+ pptx: Object.freeze({ title: 38, body: 17, caption: 12, canvasWidth: 960, canvasHeight: 540 }),
119
+ docx: Object.freeze({ title: 26, heading1: 17, heading2: 13, body: 10, margin: 57.6 }),
120
+ xlsx: Object.freeze({ title: 18, heading: 11, body: 10, tableStyle: 'TableStyleMedium4' }),
121
+ pdf: Object.freeze({ title: 27, heading: 17, body: 10, margin: 50 }),
122
+ }),
123
+ }),
124
+ });
125
+
126
+ const DEFAULT_PROFILE = Object.freeze({
127
+ docx: 'editorial',
128
+ xlsx: 'data',
129
+ pptx: 'editorial',
130
+ pdf: 'editorial',
131
+ csv: 'data',
132
+ tsv: 'data',
133
+ });
134
+
135
+ const PPTX_COMPONENTS = Object.freeze([
136
+ 'cover',
137
+ 'statement',
138
+ 'content',
139
+ 'split',
140
+ 'comparison',
141
+ 'metrics',
142
+ 'process',
143
+ 'chart',
144
+ 'table',
145
+ 'closing',
146
+ ]);
147
+
148
+ function plainObject(value) {
149
+ return value && typeof value === 'object' && !Array.isArray(value);
150
+ }
151
+
152
+ function clone(value) {
153
+ if (Array.isArray(value)) return value.map(clone);
154
+ if (!plainObject(value)) return value;
155
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
156
+ }
157
+
158
+ function merge(base, override) {
159
+ if (!plainObject(override)) return clone(base);
160
+ const result = clone(base);
161
+ for (const [key, value] of Object.entries(override)) {
162
+ result[key] = plainObject(value) && plainObject(result[key])
163
+ ? merge(result[key], value)
164
+ : clone(value);
165
+ }
166
+ return result;
167
+ }
168
+
169
+ function hex(value, fallback) {
170
+ const normalized = String(value || '').replace(/^#/, '').toUpperCase();
171
+ return /^[0-9A-F]{6}$/.test(normalized) ? normalized : fallback;
172
+ }
173
+
174
+ function strings(value) {
175
+ if (Array.isArray(value)) return value.map((entry) => String(entry ?? '')).filter(Boolean);
176
+ if (value == null || value === '') return [];
177
+ return [String(value)];
178
+ }
179
+
180
+ function slideNumbers(value) {
181
+ return [...new Set((Array.isArray(value) ? value : [])
182
+ .map((entry) => Number(entry))
183
+ .filter((entry) => Number.isInteger(entry) && entry > 0))]
184
+ .sort((left, right) => left - right);
185
+ }
186
+
187
+ function resolvePptxDeckPlan(input, tokens) {
188
+ const source = plainObject(input.deck) ? input.deck : {};
189
+ const backgroundMode = PPTX_BACKGROUND_MODES.has(String(source.backgroundMode || '').toLowerCase())
190
+ ? String(source.backgroundMode).toLowerCase()
191
+ : 'sandwich';
192
+ const defaults = backgroundMode === 'dark'
193
+ ? { cover: 'inverse', content: 'inverse', section: 'inverse', closing: 'inverse' }
194
+ : backgroundMode === 'light'
195
+ ? { cover: 'canvas', content: 'canvas', section: 'canvas', closing: 'canvas' }
196
+ : { cover: 'inverse', content: 'canvas', section: 'inverse', closing: 'inverse' };
197
+ const requestedRoles = plainObject(source.roles) ? source.roles : {};
198
+ const roles = Object.fromEntries(Object.entries(defaults).map(([key, fallback]) => {
199
+ const requested = String(requestedRoles[key] || '').trim();
200
+ return [key, Object.hasOwn(tokens.colors, requested) ? requested : fallback];
201
+ }));
202
+ const dominantColorRole = String(source.dominantColorRole || roles.content);
203
+ const requestedTemplateMode = String(source.templateMode || '').trim().toLowerCase();
204
+ return {
205
+ backgroundMode,
206
+ dominantColorRole: Object.hasOwn(tokens.colors, dominantColorRole) ? dominantColorRole : roles.content,
207
+ motif: String(source.motif || input.signature || ''),
208
+ spacingScale: String(source.spacingScale || 'consistent'),
209
+ sectionSlides: slideNumbers(source.sectionSlides),
210
+ roles,
211
+ templateMode: PPTX_TEMPLATE_MODES.has(requestedTemplateMode) ? requestedTemplateMode : 'prefer',
212
+ enforce: source.enforce !== false,
213
+ requireSlidePlan: source.requireSlidePlan !== false,
214
+ };
215
+ }
216
+
217
+ function pptxBackgroundSpec(operation, design, kind, slide) {
218
+ const deck = design.deck;
219
+ const slideRole = String(operation.slideRole || (
220
+ kind === 'cover' || kind === 'closing'
221
+ ? kind
222
+ : deck.sectionSlides.includes(slide)
223
+ ? 'section'
224
+ : 'content'
225
+ )).toLowerCase();
226
+ const fallbackRole = deck.roles[slideRole] || deck.roles.content;
227
+ const requestedRole = String(operation.backgroundRole || '').trim();
228
+ if (requestedRole && !Object.hasOwn(design.tokens.colors, requestedRole)) {
229
+ throw new Error(`Unknown PPTX background role "${requestedRole}"`);
230
+ }
231
+ const backgroundRole = requestedRole || fallbackRole;
232
+ const fallbackColor = design.tokens.colors[backgroundRole] || design.tokens.colors.canvas;
233
+ return {
234
+ slideRole,
235
+ backgroundRole,
236
+ color: hex(operation.background, fallbackColor),
237
+ custom: Boolean(operation.background),
238
+ };
239
+ }
240
+
241
+ function pptxSlidePlan(operation, kind, slide) {
242
+ const source = plainObject(operation.plan) ? operation.plan : {};
243
+ const defaults = {
244
+ cover: ['statement', 'left'],
245
+ statement: ['statement', 'left'],
246
+ content: ['diagram', 'right'],
247
+ split: [operation.image || operation.imagePath ? 'image' : 'diagram', 'right'],
248
+ comparison: ['comparison', 'full'],
249
+ metrics: ['metrics', 'full'],
250
+ process: ['process', 'full'],
251
+ closing: ['statement', 'left'],
252
+ };
253
+ const [defaultVisualType, defaultFocalRegion] = defaults[kind] || defaults.content;
254
+ const requestedVisualType = String(source.visualType || defaultVisualType).toLowerCase();
255
+ const requestedFocalRegion = String(source.focalRegion || defaultFocalRegion).toLowerCase();
256
+ const evidence = strings(source.evidence).length
257
+ ? strings(source.evidence)
258
+ : Array.isArray(operation.metrics)
259
+ ? operation.metrics.map((entry) => String(entry?.value ?? entry?.label ?? '')).filter(Boolean)
260
+ : Array.isArray(operation.steps)
261
+ ? operation.steps.map((entry) => String(entry?.title ?? entry?.label ?? '')).filter(Boolean)
262
+ : Array.isArray(operation.columns)
263
+ ? operation.columns.map((entry) => String(entry?.title ?? '')).filter(Boolean)
264
+ : strings(operation.visualText || operation.body || operation.bullets).slice(0, 3);
265
+ return {
266
+ slide,
267
+ message: String(source.message || operation.title || ''),
268
+ evidence,
269
+ visualType: PPTX_VISUAL_TYPES.has(requestedVisualType) ? requestedVisualType : defaultVisualType,
270
+ focalRegion: PPTX_FOCAL_REGIONS.has(requestedFocalRegion) ? requestedFocalRegion : defaultFocalRegion,
271
+ readingOrder: strings(source.readingOrder).length
272
+ ? strings(source.readingOrder)
273
+ : ['message', 'visual', 'support'],
274
+ hierarchy: strings(source.hierarchy).length
275
+ ? strings(source.hierarchy)
276
+ : ['message', 'visual', 'support'],
277
+ source: plainObject(operation.plan) ? 'explicit' : 'inferred',
278
+ };
279
+ }
280
+
281
+ function compactDesign(design) {
282
+ return {
283
+ profile: design.profile,
284
+ label: design.label,
285
+ intent: design.intent,
286
+ audience: design.audience,
287
+ tone: design.tone,
288
+ density: design.density,
289
+ signature: design.signature,
290
+ source: design.source,
291
+ library: design.library,
292
+ tokens: design.tokens,
293
+ format: design.format,
294
+ deck: design.deck,
295
+ content: summarizeOfficeContentModel(design.content),
296
+ slidePlans: design.slidePlans,
297
+ review: design.review,
298
+ };
299
+ }
300
+
301
+ function compactLibrary(library) {
302
+ if (!plainObject(library)) {
303
+ return {
304
+ source: 'mixdog-starter',
305
+ pack: null,
306
+ template: null,
307
+ pinned: false,
308
+ warning: '',
309
+ };
310
+ }
311
+ return {
312
+ source: String(library.source || 'mixdog-starter'),
313
+ pack: library.pack ? {
314
+ id: String(library.pack.id || ''),
315
+ version: String(library.pack.version || ''),
316
+ keyId: String(library.pack.keyId || ''),
317
+ } : null,
318
+ template: library.template ? {
319
+ id: String(library.template.id || ''),
320
+ version: String(library.template.version || ''),
321
+ source: String(library.template.source || ''),
322
+ ...(library.coverage ? { coverage: clone(library.coverage) } : {}),
323
+ } : null,
324
+ templateIndexRevision: String(library.templateIndexRevision || ''),
325
+ pinned: library.pinned === true,
326
+ warning: String(library.warning || ''),
327
+ };
328
+ }
329
+
330
+ function resolvedDesignPacks(library) {
331
+ const remote = plainObject(library?.pack?.profiles) ? library.pack.profiles : {};
332
+ const output = { ...DESIGN_PACKS };
333
+ for (const [id, profile] of Object.entries(remote)) {
334
+ const parentId = String(profile?.extends || (DESIGN_PACKS[id] ? id : 'editorial')).toLowerCase();
335
+ const parent = DESIGN_PACKS[parentId] || DESIGN_PACKS.editorial;
336
+ output[id] = merge({
337
+ ...parent,
338
+ id,
339
+ label: profile?.label || id,
340
+ }, profile);
341
+ }
342
+ return output;
343
+ }
344
+
345
+ export function officeDesignCatalog(format = '', { library = null } = {}) {
346
+ const normalized = String(format || '').toLowerCase();
347
+ if (normalized && !FORMATS.has(normalized)) throw new Error(`Unsupported Office design format: ${format}`);
348
+ return Object.values(resolvedDesignPacks(library)).map((pack) => ({
349
+ id: pack.id,
350
+ label: pack.label,
351
+ bestFor: pack.bestFor,
352
+ formats: Object.keys(pack.formats),
353
+ ...(normalized && pack.formats[normalized] ? { defaults: clone(pack.formats[normalized]) } : {}),
354
+ }));
355
+ }
356
+
357
+ export function resolveOfficeDesign(format, request = {}, { library = null } = {}) {
358
+ const normalizedFormat = String(format || '').toLowerCase();
359
+ if (!FORMATS.has(normalizedFormat)) throw new Error(`Unsupported Office design format: ${format}`);
360
+ const input = typeof request === 'string'
361
+ ? { profile: request }
362
+ : plainObject(request)
363
+ ? request
364
+ : {};
365
+ const packs = resolvedDesignPacks(library);
366
+ const libraryDefault = library?.pack?.defaultProfiles?.[normalizedFormat];
367
+ const profile = String(input.profile || libraryDefault || DEFAULT_PROFILE[normalizedFormat] || 'editorial').toLowerCase();
368
+ const pack = packs[profile];
369
+ if (!pack) {
370
+ throw new Error(`Unknown Office design profile "${profile}". Use one of: ${Object.keys(packs).join(', ')}`);
371
+ }
372
+ const palette = plainObject(input.palette) ? input.palette : {};
373
+ const typography = plainObject(input.typography) ? input.typography : {};
374
+ const tokens = merge(pack.tokens, {
375
+ colors: Object.fromEntries(Object.entries(palette).map(([key, value]) => [
376
+ key,
377
+ hex(value, pack.tokens.colors[key] || pack.tokens.colors.ink),
378
+ ])),
379
+ typography,
380
+ });
381
+ const deck = normalizedFormat === 'pptx' ? resolvePptxDeckPlan(input, tokens) : null;
382
+ return {
383
+ profile,
384
+ label: pack.label,
385
+ bestFor: pack.bestFor,
386
+ intent: String(input.intent || ''),
387
+ audience: String(input.audience || ''),
388
+ tone: String(input.tone || ''),
389
+ density: ['light', 'balanced', 'dense'].includes(String(input.density || '').toLowerCase())
390
+ ? String(input.density).toLowerCase()
391
+ : 'balanced',
392
+ signature: String(input.signature || ''),
393
+ source: String(input.source || library?.source || 'mixdog-starter'),
394
+ content: normalizeOfficeContentModel(input.content),
395
+ library: compactLibrary(library),
396
+ layouts: Array.isArray(library?.layouts) ? clone(library.layouts) : [],
397
+ tokens,
398
+ format: clone(pack.formats[normalizedFormat] || {}),
399
+ ...(deck ? { deck } : {}),
400
+ ...(normalizedFormat === 'pptx' ? {
401
+ slidePlans: Array.isArray(input.slidePlans) ? clone(input.slidePlans) : [],
402
+ } : {}),
403
+ review: {
404
+ required: normalizedFormat === 'pptx'
405
+ ? true
406
+ : input.review !== false && normalizedFormat !== 'csv' && normalizedFormat !== 'tsv',
407
+ allowTextOnly: input.allowTextOnly === true,
408
+ allowRepetition: input.allowRepetition === true,
409
+ allowDecorativeLines: input.allowDecorativeLines === true,
410
+ allowSyntheticVisuals: input.allowSyntheticVisuals === true,
411
+ },
412
+ };
413
+ }
414
+
415
+ function pptxRequiredCapabilities(operation) {
416
+ const output = [];
417
+ if (operation.image || operation.imagePath) output.push('image');
418
+ if (plainObject(operation.chart) || operation.chart === true) output.push('chart');
419
+ if (plainObject(operation.table) || Array.isArray(operation.table)) output.push('table');
420
+ return output;
421
+ }
422
+
423
+ function pptxOperationText(operation) {
424
+ return [
425
+ operation.title,
426
+ operation.subtitle,
427
+ operation.takeaway,
428
+ operation.eyebrow,
429
+ operation.visualText,
430
+ operation.chart?.title,
431
+ ...strings(operation.body),
432
+ ...strings(operation.bullets),
433
+ ...strings(operation.meta),
434
+ ...(operation.metrics || []).flatMap((entry) => [entry?.value, entry?.label, entry?.detail]),
435
+ ...(operation.columns || []).flatMap((entry) => [entry?.title, ...strings(entry?.items || entry?.body)]),
436
+ ...(operation.steps || []).flatMap((entry) => [entry?.title || entry?.label, entry?.detail || entry?.body]),
437
+ ].filter((entry) => entry !== undefined && entry !== null).map(String);
438
+ }
439
+
440
+ function pptxOperationDemand(operation, design) {
441
+ const text = pptxOperationText(operation);
442
+ const textChars = text.reduce((total, entry) => total + entry.length, 0);
443
+ const explicitDensity = String(operation.density || '').toLowerCase();
444
+ const density = ['light', 'balanced', 'dense'].includes(explicitDensity)
445
+ ? explicitDensity
446
+ : textChars > 340 ? 'dense' : textChars > 120 ? 'balanced' : design.density;
447
+ return {
448
+ density,
449
+ textChars,
450
+ textItems: text.filter((entry) => entry.trim()).length,
451
+ metrics: Array.isArray(operation.metrics) ? operation.metrics.length : 0,
452
+ columns: Array.isArray(operation.columns) ? operation.columns.length : 0,
453
+ steps: Array.isArray(operation.steps) ? operation.steps.length : 0,
454
+ capabilities: pptxRequiredCapabilities(operation),
455
+ };
456
+ }
457
+
458
+ function layoutNumberedGroups(layout, prefix) {
459
+ return new Set((layout.slots || []).flatMap((slot) => {
460
+ const match = new RegExp(`^${prefix}-(?:value|label|detail|title|body)-(\\d+)$`).exec(String(slot.role || ''));
461
+ return match ? [Number(match[1])] : [];
462
+ })).size;
463
+ }
464
+
465
+ function densityDistance(left, right) {
466
+ const order = ['light', 'balanced', 'dense'];
467
+ const leftIndex = order.indexOf(left);
468
+ const rightIndex = order.indexOf(right);
469
+ return leftIndex >= 0 && rightIndex >= 0 ? Math.abs(leftIndex - rightIndex) : 1;
470
+ }
471
+
472
+ function pptxLayoutFit(layout, operation, design, usage) {
473
+ const demand = pptxOperationDemand(operation, design);
474
+ const slots = Array.isArray(layout.slots) ? layout.slots : [];
475
+ const capacity = layout.capacity || {};
476
+ const available = {
477
+ metrics: Math.max(Number(capacity.metricGroups) || 0, layoutNumberedGroups(layout, 'metric')),
478
+ columns: Math.max(Number(capacity.columnGroups) || 0, layoutNumberedGroups(layout, 'column')),
479
+ steps: Math.max(Number(capacity.stepGroups) || 0, layoutNumberedGroups(layout, 'step')),
480
+ };
481
+ const missingGroups = Object.entries({
482
+ metrics: Math.max(0, demand.metrics - available.metrics),
483
+ columns: Math.max(0, demand.columns - available.columns),
484
+ steps: Math.max(0, demand.steps - available.steps),
485
+ }).filter(([, count]) => count > 0);
486
+ const nativeSample = Boolean(layout.templatePath && Number(layout.sourceSlide) > 0);
487
+ const densityPenalty = densityDistance(layout.density || design.density, demand.density);
488
+ const textSlots = Math.max(Number(capacity.textSlots) || 0, slots.filter((slot) => slot.type === 'text').length);
489
+ const textSlotShortfall = nativeSample ? Math.max(0, demand.textItems - textSlots) : 0;
490
+ const score = (
491
+ (layout.profile === design.profile ? 4 : 0)
492
+ + (layout.density === demand.density ? 7 : 0)
493
+ - densityPenalty * 2
494
+ + (String(operation.variant || '').toLowerCase() && layout.variant === String(operation.variant).toLowerCase() ? 2 : 0)
495
+ + demand.capabilities.filter((capability) => layout.capabilities?.includes(capability)).length * 4
496
+ + Math.min(demand.metrics, available.metrics) * 3
497
+ + Math.min(demand.columns, available.columns) * 3
498
+ + Math.min(demand.steps, available.steps) * 3
499
+ - missingGroups.reduce((total, [, count]) => total + count * 8, 0)
500
+ - textSlotShortfall * 2
501
+ + (Number(layout.priority) || 0)
502
+ - ((usage.get(layout.id) || 0) * 5)
503
+ );
504
+ return {
505
+ layout,
506
+ score,
507
+ demand,
508
+ fit: {
509
+ density: layout.density || '',
510
+ available,
511
+ missingGroups: Object.fromEntries(missingGroups),
512
+ textSlots,
513
+ textSlotShortfall,
514
+ nativeSample,
515
+ },
516
+ };
517
+ }
518
+
519
+ function selectPptxLayout(operation, design, usage = new Map()) {
520
+ const layouts = Array.isArray(design.layouts) ? design.layouts : [];
521
+ const requestedId = String(operation.layoutId || '').toLowerCase();
522
+ if (requestedId) {
523
+ const selected = layouts.find((layout) => layout.id === requestedId);
524
+ if (!selected) throw new Error(`Unknown Office semantic layout "${operation.layoutId}" for the pinned design library`);
525
+ return pptxLayoutFit(selected, operation, design, usage);
526
+ }
527
+ const kind = String(operation.kind || 'content').toLowerCase();
528
+ const variant = String(operation.variant || '').toLowerCase();
529
+ const requiredCapabilities = pptxRequiredCapabilities(operation);
530
+ const candidates = layouts.filter((layout) => (
531
+ layout.format === 'pptx'
532
+ && layout.kind === kind
533
+ && (!layout.profile || layout.profile === design.profile)
534
+ && (!variant || !layout.variant || layout.variant === variant)
535
+ && requiredCapabilities.every((capability) => layout.capabilities?.includes(capability))
536
+ ));
537
+ const scored = candidates.map((layout) => pptxLayoutFit(layout, operation, design, usage));
538
+ const viable = scored.filter((entry) => (
539
+ !entry.fit.nativeSample || Object.keys(entry.fit.missingGroups).length === 0
540
+ ));
541
+ const ranked = design.deck?.templateMode === 'strict' ? viable : (viable.length ? viable : scored);
542
+ ranked.sort((left, right) => right.score - left.score || left.layout.id.localeCompare(right.layout.id));
543
+ return ranked[0] || null;
544
+ }
545
+
546
+ function templateSlotText(operation, role) {
547
+ const metrics = Array.isArray(operation.metrics) ? operation.metrics : [];
548
+ const columns = Array.isArray(operation.columns) ? operation.columns : [];
549
+ const steps = Array.isArray(operation.steps) ? operation.steps : [];
550
+ const indexed = (pattern, values) => {
551
+ const match = pattern.exec(role);
552
+ return match ? values[Number(match[2]) - 1]?.[match[1]] : undefined;
553
+ };
554
+ if (role === 'title') return operation.title;
555
+ if (role === 'subtitle') return operation.subtitle;
556
+ if (role === 'eyebrow') return operation.eyebrow;
557
+ if (role === 'body') return strings(operation.body || operation.bullets).join('\r');
558
+ if (role === 'visual-text') return operation.visualText;
559
+ if (role === 'meta') return strings(operation.meta).join(' · ');
560
+ let value = indexed(/^metric-(value|label|detail)-(\d+)$/, metrics);
561
+ if (value != null) return value;
562
+ value = indexed(/^column-(title|body)-(\d+)$/, columns.map((column) => ({
563
+ title: column?.title,
564
+ body: strings(column?.items || column?.body).join('\r'),
565
+ })));
566
+ if (value != null) return value;
567
+ value = indexed(/^step-(title|detail)-(\d+)$/, steps.map((step) => ({
568
+ title: step?.title || step?.label,
569
+ detail: step?.detail || step?.body,
570
+ })));
571
+ if (value != null) return value;
572
+ return undefined;
573
+ }
574
+
575
+ function expandTemplatePptxSlide(operation, layout, slide) {
576
+ if (
577
+ operation.create === false
578
+ || !layout?.templatePath
579
+ || !Number.isInteger(Number(layout.sourceSlide))
580
+ || Number(layout.sourceSlide) < 1
581
+ ) return null;
582
+ const output = [{
583
+ op: 'import_slides',
584
+ path: layout.templatePath,
585
+ slides: [Number(layout.sourceSlide)],
586
+ ...(slide > 1 ? { after: slide - 1 } : {}),
587
+ }];
588
+ if (operation.background) {
589
+ output.push({
590
+ op: 'set_slide_background',
591
+ slide,
592
+ color: String(operation.background),
593
+ });
594
+ }
595
+ const imagePath = operation.image?.path || operation.imagePath;
596
+ for (const slot of layout.slots || []) {
597
+ if (slot.type === 'text') {
598
+ const value = templateSlotText(operation, slot.role);
599
+ output.push({
600
+ op: 'set_text',
601
+ slide,
602
+ shape: Number(slot.shape),
603
+ text: String(value ?? ''),
604
+ ...(value == null || value === '' ? { allowNoChange: true } : {}),
605
+ });
606
+ } else if (slot.type === 'image' && imagePath) {
607
+ output.push({
608
+ op: 'replace_image',
609
+ slide,
610
+ shape: Number(slot.shape),
611
+ path: imagePath,
612
+ });
613
+ } else if (slot.type === 'chart' && plainObject(operation.chart)) {
614
+ output.push({
615
+ op: 'set_chart_data',
616
+ slide,
617
+ shape: Number(slot.shape),
618
+ series: Array.isArray(operation.chart.series) ? operation.chart.series : [],
619
+ ...(Array.isArray(operation.chart.categories) ? { categories: operation.chart.categories } : {}),
620
+ ...(operation.chart.title ? { title: String(operation.chart.title) } : {}),
621
+ });
622
+ } else if (slot.type === 'table') {
623
+ const values = Array.isArray(operation.table)
624
+ ? operation.table
625
+ : operation.table?.values;
626
+ if (Array.isArray(values)) {
627
+ output.push({
628
+ op: 'set_table_data',
629
+ slide,
630
+ shape: Number(slot.shape),
631
+ values,
632
+ });
633
+ }
634
+ }
635
+ }
636
+ if (operation.notes) output.push({ op: 'set_notes', slide, text: String(operation.notes) });
637
+ return output;
638
+ }
639
+
640
+ function coalesceCreatedPptxTemplateImports(operations) {
641
+ const imports = operations.filter((operation) => operation.op === 'import_slides');
642
+ if (imports.length < 2 || operations[0]?.op !== 'import_slides') return operations;
643
+ const path = imports[0].path;
644
+ if (!imports.every((operation, index) => (
645
+ operation.path === path
646
+ && Array.isArray(operation.slides)
647
+ && operation.slides.length === 1
648
+ && Number(operation.after || 0) === index
649
+ ))) return operations;
650
+ const first = {
651
+ ...imports[0],
652
+ slides: imports.map((operation) => Number(operation.slides[0])),
653
+ };
654
+ delete first.after;
655
+ let emitted = false;
656
+ return operations.flatMap((operation) => {
657
+ if (operation.op !== 'import_slides') return [operation];
658
+ if (emitted) return [];
659
+ emitted = true;
660
+ return [first];
661
+ });
662
+ }
663
+
664
+ function pptText(slide, text, properties = {}, paragraphs = null) {
665
+ return {
666
+ op: 'add_textbox',
667
+ slide,
668
+ text: String(text || ''),
669
+ ...(paragraphs?.length ? { paragraphs } : {}),
670
+ properties: {
671
+ marginLeft: 0,
672
+ marginTop: 0,
673
+ marginRight: 0,
674
+ marginBottom: 0,
675
+ ...properties,
676
+ },
677
+ };
678
+ }
679
+
680
+ function pptShape(slide, shapeType, properties = {}, text = '') {
681
+ return {
682
+ op: 'add_shape',
683
+ slide,
684
+ shapeType,
685
+ text,
686
+ properties,
687
+ };
688
+ }
689
+
690
+ function pptBodyParagraphs(body, design, { size, color } = {}) {
691
+ return strings(body).map((text) => ({
692
+ text,
693
+ fontName: design.tokens.typography.body,
694
+ fontSize: size || design.format.body,
695
+ color: color || design.tokens.colors.ink,
696
+ breakLine: true,
697
+ }));
698
+ }
699
+
700
+ function pptBulletParagraphs(body, design, { size, color } = {}) {
701
+ return strings(body).map((text) => ({
702
+ text,
703
+ bullet: true,
704
+ fontName: design.tokens.typography.body,
705
+ fontSize: size || design.format.body,
706
+ color: color || design.tokens.colors.ink,
707
+ breakLine: true,
708
+ }));
709
+ }
710
+
711
+ function pptTitleSize(text, preferred = 34) {
712
+ const length = [...String(text || '').trim()].reduce(
713
+ (total, character) => total + (/[\u2E80-\u9FFF\uAC00-\uD7AF]/u.test(character) ? 1.7 : 1),
714
+ 0,
715
+ );
716
+ if (length > 64) return Math.min(preferred, 25);
717
+ if (length > 48) return Math.min(preferred, 28);
718
+ if (length > 34) return Math.min(preferred, 31);
719
+ return preferred;
720
+ }
721
+
722
+ function metricDisplayValue(metric) {
723
+ const value = metric?.display ?? metric?.value;
724
+ if (typeof value !== 'number' || !Number.isFinite(value)) return String(value ?? '');
725
+ const format = String(metric?.numberFormat || '');
726
+ const decimals = Math.max(0, (format.match(/0\.(0+)/)?.[1] || '').length);
727
+ if (format.includes('%')) return `${(value * 100).toFixed(decimals)}%`;
728
+ if (!format || format === 'General') return String(value);
729
+ return new Intl.NumberFormat('en-US', {
730
+ useGrouping: format.includes(','),
731
+ minimumFractionDigits: decimals,
732
+ maximumFractionDigits: decimals,
733
+ }).format(value);
734
+ }
735
+
736
+ function provenanceText(source) {
737
+ if (!source) return '';
738
+ if (typeof source === 'string') return source.trim();
739
+ if (!plainObject(source)) return '';
740
+ const document = String(source.document || source.label || '').trim();
741
+ const target = String(source.target || '').trim();
742
+ if (!document) return '';
743
+ return target ? `${document}#${target}` : document;
744
+ }
745
+
746
+ function expandPptxSlide(operation, design, slide) {
747
+ const colors = design.tokens.colors;
748
+ const type = design.tokens.typography;
749
+ const kind = String(operation.kind || 'content').toLowerCase();
750
+ if (!PPTX_COMPONENTS.includes(kind)) {
751
+ throw new Error(`Unknown compose_slide kind "${kind}". Use one of: ${PPTX_COMPONENTS.join(', ')}`);
752
+ }
753
+ const output = [];
754
+ if (operation.create !== false) {
755
+ output.push({
756
+ op: 'add_slide',
757
+ ...(operation.index ? { index: Number(operation.index) } : {}),
758
+ ...(operation.layout ? { layout: operation.layout } : {}),
759
+ });
760
+ }
761
+ const backgroundSpec = pptxBackgroundSpec(operation, design, kind, slide);
762
+ const background = backgroundSpec.color;
763
+ output.push({ op: 'set_slide_background', slide, color: background });
764
+ const inverseBackground = backgroundSpec.backgroundRole === 'inverse';
765
+ const titleColor = inverseBackground ? colors.onInverse : colors.ink;
766
+ const mutedColor = inverseBackground ? colors.surface2 : colors.muted;
767
+ const title = String(operation.title || '');
768
+ const subtitle = String(operation.takeaway || operation.subtitle || '');
769
+ const eyebrow = String(operation.eyebrow || '');
770
+ const contentTitleSize = pptTitleSize(title, Number(operation.titleSize) || 34);
771
+
772
+ if (kind === 'cover') {
773
+ if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
774
+ left: 58, top: 58, width: 420, height: 24,
775
+ fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
776
+ }));
777
+ output.push(pptText(slide, title, {
778
+ left: 58, top: 126, width: 720, height: 170,
779
+ fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || 44), bold: true, color: colors.onInverse,
780
+ }));
781
+ if (subtitle) output.push(pptText(slide, subtitle, {
782
+ left: 58, top: 326, width: 650, height: 72,
783
+ fontName: type.body, fontSize: 18, color: mutedColor,
784
+ }));
785
+ const meta = strings(operation.meta);
786
+ if (meta.length) output.push(pptText(slide, meta.join(' · '), {
787
+ left: 58, top: 474, width: 720, height: 22,
788
+ fontName: type.body, fontSize: 12, color: mutedColor,
789
+ }));
790
+ output.push(pptShape(slide, 'oval', {
791
+ left: 734, top: 92, width: 62, height: 62,
792
+ fillColor: colors.accent, lineColor: colors.accent,
793
+ }));
794
+ output.push(pptShape(slide, 'right_arrow', {
795
+ left: 802, top: 110, width: 46, height: 26,
796
+ fillColor: colors.accent2, lineColor: colors.accent2,
797
+ }));
798
+ output.push(pptShape(slide, 'oval', {
799
+ left: 854, top: 92, width: 62, height: 62,
800
+ fillColor: colors.surface2, lineColor: colors.surface2,
801
+ }));
802
+ } else if (kind === 'statement') {
803
+ output.push(pptShape(slide, 'oval', {
804
+ left: 714, top: 154, width: 164, height: 164,
805
+ fillColor: colors.surface, lineColor: colors.surface,
806
+ }));
807
+ if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
808
+ left: 58, top: 52, width: 420, height: 22,
809
+ fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
810
+ }));
811
+ const metric = operation.metric || operation.metrics?.[0] || {};
812
+ if (metric.value != null) output.push(pptText(slide, String(metric.value), {
813
+ left: 674, top: 188, width: 244, height: 92,
814
+ fontName: type.display, fontSize: 64, bold: true, color: colors.accent, alignment: 'center',
815
+ }));
816
+ if (metric.label) output.push(pptText(slide, String(metric.label), {
817
+ left: 734, top: 286, width: 124, height: 24,
818
+ fontName: type.body, fontSize: 12, bold: true, color: colors.muted, alignment: 'center',
819
+ }));
820
+ output.push(pptText(slide, title, {
821
+ left: 58, top: 144, width: metric.value != null ? 560 : 790, height: 150,
822
+ fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || 38), bold: true, color: colors.ink,
823
+ }));
824
+ if (subtitle) output.push(pptText(slide, subtitle, {
825
+ left: 58, top: 356, width: 650, height: 76,
826
+ fontName: type.body, fontSize: 17, color: colors.muted,
827
+ }));
828
+ } else if (kind === 'metrics') {
829
+ output.push(pptText(slide, title, {
830
+ left: 58, top: 42, width: 842, height: 76,
831
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
832
+ }));
833
+ if (subtitle) output.push(pptText(slide, subtitle, {
834
+ left: 58, top: 114, width: 760, height: 42,
835
+ fontName: type.body, fontSize: 15, color: colors.muted,
836
+ }));
837
+ const metrics = Array.isArray(operation.metrics) ? operation.metrics.slice(0, 4) : [];
838
+ metrics.forEach((metric, index) => {
839
+ const primary = index === 0;
840
+ const left = primary ? 58 : 520;
841
+ const top = primary ? 208 : 192 + ((index - 1) * 118);
842
+ output.push(pptText(slide, metricDisplayValue(metric), {
843
+ left, top, width: primary ? 392 : 320, height: primary ? 96 : 62,
844
+ fontName: type.display, fontSize: primary ? 58 : 40, bold: true,
845
+ color: primary ? colors.accent : colors.ink,
846
+ }));
847
+ output.push(pptText(slide, String(metric.label || ''), {
848
+ left, top: top + (primary ? 104 : 58), width: primary ? 360 : 320, height: 32,
849
+ fontName: type.body, fontSize: 13, bold: true, color: colors.muted,
850
+ }));
851
+ if (metric.detail) output.push(pptText(slide, String(metric.detail), {
852
+ left, top: top + (primary ? 148 : 86), width: primary ? 390 : 340, height: 38,
853
+ fontName: type.body, fontSize: 12, color: colors.muted,
854
+ }));
855
+ if (!primary) output.push(pptShape(slide, 'oval', {
856
+ left: 486, top: top + 16, width: 14, height: 14,
857
+ fillColor: index === 1 ? colors.accent2 : colors.surface2,
858
+ lineColor: index === 1 ? colors.accent2 : colors.surface2,
859
+ }));
860
+ });
861
+ } else if (kind === 'comparison') {
862
+ output.push(pptText(slide, title, {
863
+ left: 58, top: 42, width: 842, height: 76,
864
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
865
+ }));
866
+ const columns = Array.isArray(operation.columns) ? operation.columns.slice(0, 2) : [];
867
+ const fills = [colors.surface, colors.inverse];
868
+ columns.forEach((column, index) => {
869
+ const left = index === 0 ? 58 : 492;
870
+ const fill = fills[index];
871
+ const foreground = index === 0 ? colors.ink : colors.onInverse;
872
+ output.push(pptShape(slide, 'rectangle', {
873
+ left, top: 142, width: 410, height: 328,
874
+ fillColor: fill, lineColor: fill,
875
+ }));
876
+ output.push(pptText(slide, String(column.title || ''), {
877
+ left: left + 28, top: 174, width: 340, height: 48,
878
+ fontName: type.display, fontSize: 24, bold: true, color: foreground,
879
+ }));
880
+ const paragraphs = pptBulletParagraphs(column.items || column.body, design, {
881
+ size: 15,
882
+ color: index === 0 ? colors.muted : colors.surface2,
883
+ });
884
+ output.push(pptText(slide, '', {
885
+ left: left + 28, top: 244, width: 340, height: 176,
886
+ }, paragraphs));
887
+ });
888
+ } else if (kind === 'process') {
889
+ output.push(pptText(slide, title, {
890
+ left: 58, top: 42, width: 842, height: 76,
891
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
892
+ }));
893
+ const steps = Array.isArray(operation.steps) ? operation.steps.slice(0, 5) : [];
894
+ const width = 820 / Math.max(1, steps.length);
895
+ steps.forEach((step, index) => {
896
+ const left = 58 + (width * index);
897
+ output.push(pptShape(slide, 'oval', {
898
+ left, top: 172, width: 58, height: 58,
899
+ fillColor: index === 0 ? colors.accent : colors.surface2,
900
+ lineColor: index === 0 ? colors.accent : colors.surface2,
901
+ }));
902
+ output.push(pptText(slide, String(index + 1).padStart(2, '0'), {
903
+ left, top: 187, width: 58, height: 30,
904
+ fontName: type.data, fontSize: 22, bold: true,
905
+ color: index === 0 ? colors.onAccent : colors.muted,
906
+ alignment: 'center',
907
+ }));
908
+ output.push(pptText(slide, String(step.title || step.label || ''), {
909
+ left: left - 24, top: 260, width: 106, height: 50,
910
+ fontName: type.body, fontSize: 15, bold: true, color: colors.ink, alignment: 'center',
911
+ }));
912
+ if (step.detail || step.body) output.push(pptText(slide, String(step.detail || step.body), {
913
+ left: left - 44, top: 324, width: 146, height: 94,
914
+ fontName: type.body, fontSize: 12, color: colors.muted, alignment: 'center',
915
+ }));
916
+ if (index < steps.length - 1) output.push(pptShape(slide, 'right_arrow', {
917
+ left: left + width - 78, top: 190, width: 52, height: 20,
918
+ fillColor: colors.accent2, lineColor: colors.accent2,
919
+ }));
920
+ });
921
+ } else if (kind === 'chart' || (kind === 'content' && plainObject(operation.chart))) {
922
+ output.push(pptText(slide, title, {
923
+ left: 58, top: 42, width: 842, height: 76,
924
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
925
+ }));
926
+ if (subtitle) output.push(pptText(slide, subtitle, {
927
+ left: 58, top: 116, width: 820, height: 40,
928
+ fontName: type.body, fontSize: 14, color: colors.muted,
929
+ }));
930
+ const paragraphs = operation.bullets
931
+ ? pptBulletParagraphs(operation.bullets, design, { size: 14 })
932
+ : pptBodyParagraphs(operation.body, design, { size: 14 });
933
+ if (paragraphs.length) output.push(pptText(slide, '', {
934
+ left: 58, top: 184, width: 274, height: 238,
935
+ }, paragraphs));
936
+ const chart = operation.chart || {};
937
+ output.push({
938
+ op: 'add_chart',
939
+ slide,
940
+ chartType: chart.type || chart.chartType || 'column',
941
+ title: chart.title || '',
942
+ categories: Array.isArray(chart.categories) ? chart.categories : [],
943
+ series: Array.isArray(chart.series) ? chart.series.map((entry, index) => ({
944
+ ...entry,
945
+ color: entry?.color || (index === 0 ? colors.accent : index === 1 ? colors.accent2 : colors.muted),
946
+ })) : [],
947
+ left: paragraphs.length ? 360 : 58,
948
+ top: 176,
949
+ width: paragraphs.length ? 540 : 842,
950
+ height: 286,
951
+ });
952
+ } else if (kind === 'table' || (kind === 'content' && (Array.isArray(operation.table) || plainObject(operation.table)))) {
953
+ output.push(pptText(slide, title, {
954
+ left: 58, top: 42, width: 842, height: 76,
955
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
956
+ }));
957
+ if (subtitle) output.push(pptText(slide, subtitle, {
958
+ left: 58, top: 116, width: 820, height: 40,
959
+ fontName: type.body, fontSize: 14, color: colors.muted,
960
+ }));
961
+ const values = Array.isArray(operation.table) ? operation.table : operation.table?.values;
962
+ const rowCount = Math.max(1, Array.isArray(values) ? values.length : 0);
963
+ const bodyRowHeight = rowCount > 1
964
+ ? Math.min(64, Math.max(40, (288 - 42) / (rowCount - 1)))
965
+ : 0;
966
+ const tableHeight = 42 + (bodyRowHeight * Math.max(0, rowCount - 1));
967
+ output.push({
968
+ op: 'add_table',
969
+ slide,
970
+ values: Array.isArray(values) ? values : [],
971
+ left: 58,
972
+ top: 174,
973
+ width: 842,
974
+ height: tableHeight,
975
+ properties: {
976
+ fontName: type.body,
977
+ fontSize: 13,
978
+ color: colors.ink,
979
+ headerFillColor: colors.inverse,
980
+ headerColor: colors.onInverse,
981
+ bodyFillColor: colors.canvas,
982
+ headerRowHeight: 42,
983
+ bodyRowHeight,
984
+ },
985
+ });
986
+ } else if (kind === 'split') {
987
+ output.push(pptText(slide, title, {
988
+ left: 58, top: 42, width: 842, height: 76,
989
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
990
+ }));
991
+ const paragraphs = operation.bullets
992
+ ? pptBulletParagraphs(operation.bullets, design, { size: 16 })
993
+ : pptBodyParagraphs(operation.body, design, { size: 16 });
994
+ output.push(pptText(slide, '', {
995
+ left: 58, top: 156, width: 374, height: 280,
996
+ }, paragraphs));
997
+ const imagePath = operation.image?.path || operation.imagePath;
998
+ if (imagePath) {
999
+ output.push({
1000
+ op: 'add_image',
1001
+ slide,
1002
+ path: imagePath,
1003
+ left: 492,
1004
+ top: 142,
1005
+ width: 408,
1006
+ height: 306,
1007
+ });
1008
+ } else {
1009
+ output.push(pptShape(slide, 'rectangle', {
1010
+ left: 492, top: 142, width: 408, height: 306,
1011
+ fillColor: colors.inverse, lineColor: colors.inverse,
1012
+ }));
1013
+ output.push(pptText(slide, String(operation.visualText || subtitle || ''), {
1014
+ left: 530, top: 214, width: 330, height: 156,
1015
+ fontName: type.display, fontSize: 30, bold: true, color: colors.onInverse,
1016
+ }));
1017
+ }
1018
+ } else if (kind === 'closing') {
1019
+ if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
1020
+ left: 58, top: 68, width: 420, height: 22,
1021
+ fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
1022
+ }));
1023
+ output.push(pptText(slide, title, {
1024
+ left: 58, top: 156, width: operation.visualText ? 650 : 760, height: 142,
1025
+ fontName: type.display, fontSize: pptTitleSize(title, 42), bold: true, color: colors.onInverse,
1026
+ }));
1027
+ if (subtitle) output.push(pptText(slide, subtitle, {
1028
+ left: 58, top: 330, width: 690, height: 72,
1029
+ fontName: type.body, fontSize: 17, color: mutedColor,
1030
+ }));
1031
+ if (operation.visualText) {
1032
+ output.push(pptShape(slide, 'oval', {
1033
+ left: 770, top: 160, width: 126, height: 126,
1034
+ fillColor: colors.accent, lineColor: colors.accent,
1035
+ }));
1036
+ output.push(pptText(slide, String(operation.visualText), {
1037
+ left: 770, top: 184, width: 126, height: 54,
1038
+ fontName: type.display, fontSize: 34, bold: true, color: colors.onAccent, alignment: 'center',
1039
+ }));
1040
+ if (operation.visualLabel) output.push(pptText(slide, String(operation.visualLabel), {
1041
+ left: 785, top: 246, width: 96, height: 24,
1042
+ fontName: type.body, fontSize: 12, bold: true, color: colors.onAccent, alignment: 'center',
1043
+ }));
1044
+ }
1045
+ } else {
1046
+ output.push(pptText(slide, title, {
1047
+ left: 58, top: 42, width: 842, height: 76,
1048
+ fontName: type.display, fontSize: contentTitleSize, bold: true, color: titleColor,
1049
+ }));
1050
+ if (subtitle) output.push(pptText(slide, subtitle, {
1051
+ left: 58, top: 116, width: 760, height: 38,
1052
+ fontName: type.body, fontSize: 14, color: mutedColor,
1053
+ }));
1054
+ const paragraphs = operation.bullets
1055
+ ? pptBulletParagraphs(operation.bullets, design)
1056
+ : pptBodyParagraphs(operation.body, design);
1057
+ output.push(pptText(slide, '', {
1058
+ left: 58, top: 178, width: 590, height: 250,
1059
+ }, paragraphs));
1060
+ const visualText = String(operation.visualText || '');
1061
+ if (visualText) {
1062
+ output.push(pptShape(slide, 'rectangle', {
1063
+ left: 680, top: 178, width: 220, height: 220,
1064
+ fillColor: colors.surface, lineColor: colors.surface,
1065
+ }));
1066
+ output.push(pptText(slide, visualText, {
1067
+ left: 710, top: 224, width: 160, height: 130,
1068
+ fontName: type.display, fontSize: 30, bold: true, color: colors.accent,
1069
+ }));
1070
+ }
1071
+ }
1072
+ const source = provenanceText(operation.source);
1073
+ const notes = [String(operation.notes || '').trim(), source ? `Source: ${source}` : ''].filter(Boolean).join('\r\n');
1074
+ if (notes) output.push({ op: 'set_notes', slide, text: notes });
1075
+ return output;
1076
+ }
1077
+
1078
+ function expandDocxDocument(operation, design, state, backend) {
1079
+ const output = [];
1080
+ const colors = design.tokens.colors;
1081
+ const type = design.tokens.typography;
1082
+ const format = design.format;
1083
+ if (backend === 'microsoft-office-com' && operation.page !== false) {
1084
+ output.push({
1085
+ op: 'set_page',
1086
+ properties: {
1087
+ orientation: operation.orientation || 'portrait',
1088
+ topMargin: format.margin,
1089
+ bottomMargin: format.margin,
1090
+ leftMargin: format.margin,
1091
+ rightMargin: format.margin,
1092
+ },
1093
+ });
1094
+ }
1095
+ const append = (text, style, properties = {}) => {
1096
+ if (text == null || text === '') return 0;
1097
+ state.paragraph += 1;
1098
+ output.push({
1099
+ op: 'append_text',
1100
+ text: String(text),
1101
+ style,
1102
+ properties,
1103
+ });
1104
+ return state.paragraph;
1105
+ };
1106
+ append(operation.title, 'Title', {
1107
+ name: type.display,
1108
+ size: Number(operation.titleSize) || format.title,
1109
+ bold: true,
1110
+ color: colors.ink,
1111
+ spacingBefore: 0,
1112
+ spacingAfter: 8,
1113
+ keepWithNext: true,
1114
+ });
1115
+ append(operation.subtitle, 'Normal', {
1116
+ name: type.body,
1117
+ size: format.body,
1118
+ color: colors.muted,
1119
+ spacingBefore: 0,
1120
+ spacingAfter: 12,
1121
+ lineSpacing: format.body * 1.35,
1122
+ });
1123
+ const meta = strings(operation.meta);
1124
+ if (meta.length) {
1125
+ append(meta.join(' · '), 'Normal', {
1126
+ name: type.body,
1127
+ size: Math.max(8.5, format.body - 1),
1128
+ color: colors.muted,
1129
+ spacingBefore: 0,
1130
+ spacingAfter: 14,
1131
+ });
1132
+ }
1133
+ if (operation.summary) {
1134
+ append(strings(operation.summary).join(' '), 'Normal', {
1135
+ name: type.display,
1136
+ size: format.body + 1,
1137
+ bold: true,
1138
+ color: colors.accent,
1139
+ spacingBefore: 4,
1140
+ spacingAfter: 14,
1141
+ lineSpacing: (format.body + 1) * 1.35,
1142
+ keepWithNext: true,
1143
+ border: { side: 'left', color: colors.accent },
1144
+ });
1145
+ }
1146
+ for (const section of Array.isArray(operation.sections) ? operation.sections : []) {
1147
+ append(section.heading || section.title, Number(section.level) === 2 ? 'Heading 2' : 'Heading 1', {
1148
+ name: type.display,
1149
+ size: Number(section.level) === 2 ? format.heading2 : format.heading1,
1150
+ bold: true,
1151
+ color: section.accent === false ? colors.ink : colors.accent,
1152
+ spacingBefore: Number(section.level) === 2 ? 9 : 14,
1153
+ spacingAfter: 5,
1154
+ keepWithNext: true,
1155
+ pageBreakBefore: section.pageBreak === true,
1156
+ });
1157
+ for (const paragraph of strings(section.paragraphs || section.body)) {
1158
+ append(paragraph, 'Normal', {
1159
+ name: type.body,
1160
+ size: format.body,
1161
+ color: colors.ink,
1162
+ spacingBefore: 0,
1163
+ spacingAfter: 6,
1164
+ lineSpacing: format.body * 1.4,
1165
+ });
1166
+ }
1167
+ for (const bullet of strings(section.bullets)) {
1168
+ append(bullet, 'Normal', {
1169
+ name: type.body,
1170
+ size: format.body,
1171
+ color: colors.ink,
1172
+ spacingBefore: 0,
1173
+ spacingAfter: 3,
1174
+ lineSpacing: format.body * 1.35,
1175
+ listKind: 'bullet',
1176
+ listLevel: 0,
1177
+ });
1178
+ }
1179
+ if (section.quote) {
1180
+ append(section.quote, 'Quote', {
1181
+ name: type.display,
1182
+ size: format.body + 1,
1183
+ italic: true,
1184
+ color: colors.accent,
1185
+ spacingBefore: 5,
1186
+ spacingAfter: 9,
1187
+ lineSpacing: (format.body + 1) * 1.35,
1188
+ });
1189
+ }
1190
+ if (Array.isArray(section.table) && section.table.length) {
1191
+ state.table += 1;
1192
+ output.push({
1193
+ op: 'add_table',
1194
+ values: section.table,
1195
+ properties: {
1196
+ style: 'Table Grid',
1197
+ textStyle: 'Normal',
1198
+ fontName: type.body,
1199
+ fontSize: Math.max(9, format.body - 1),
1200
+ color: colors.ink,
1201
+ spacingAfter: 0,
1202
+ borders: true,
1203
+ },
1204
+ });
1205
+ if (backend === 'microsoft-office-com') {
1206
+ output.push({ op: 'fit_table', table: state.table });
1207
+ const columns = Math.max(1, ...section.table.map((row) => Array.isArray(row) ? row.length : 1));
1208
+ for (let column = 1; column <= columns; column += 1) {
1209
+ output.push({
1210
+ op: 'set_table_cell_style',
1211
+ table: state.table,
1212
+ row: 1,
1213
+ col: column,
1214
+ properties: {
1215
+ fillColor: colors.inverse,
1216
+ color: colors.onInverse,
1217
+ bold: true,
1218
+ verticalAlignment: 'center',
1219
+ },
1220
+ });
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ if (backend === 'microsoft-office-com' && operation.footer) {
1226
+ output.push({ op: 'set_header_footer', header: false, text: String(operation.footer) });
1227
+ }
1228
+ if (backend === 'microsoft-office-com' && operation.pageNumbers === true) {
1229
+ output.push({ op: 'add_page_numbers', includeTotal: true });
1230
+ }
1231
+ return output;
1232
+ }
1233
+
1234
+ function columnLabel(column) {
1235
+ let value = Math.max(1, Number(column) || 1);
1236
+ let label = '';
1237
+ while (value > 0) {
1238
+ value -= 1;
1239
+ label = String.fromCharCode(65 + (value % 26)) + label;
1240
+ value = Math.floor(value / 26);
1241
+ }
1242
+ return label;
1243
+ }
1244
+
1245
+ function safeTableName(value) {
1246
+ const normalized = String(value || 'MixdogTable').replace(/[^A-Za-z0-9_]/g, '');
1247
+ const leading = /^[A-Za-z_]/.test(normalized) ? normalized : `T${normalized}`;
1248
+ return (leading || 'MixdogTable').slice(0, 240);
1249
+ }
1250
+
1251
+ function isExcelTotalRow(row) {
1252
+ return /^(?:(?:grand\s+total|sub\s*total|total)\b|(?:합계|총계|소계)(?:\s|$))/i.test(String(row?.[0] || '').trim());
1253
+ }
1254
+
1255
+ function expandXlsxSheet(operation, design) {
1256
+ const output = [];
1257
+ const colors = design.tokens.colors;
1258
+ const type = design.tokens.typography;
1259
+ const format = design.format;
1260
+ const sheet = String(operation.sheet || 'Sheet1');
1261
+ const headers = strings(operation.headers);
1262
+ const rows = Array.isArray(operation.rows) ? operation.rows : [];
1263
+ const metrics = Array.isArray(operation.metrics) ? operation.metrics.slice(0, 4) : [];
1264
+ const dashboard = String(operation.kind || '').toLowerCase() === 'dashboard' || metrics.length > 0;
1265
+ const dataColumns = Math.max(
1266
+ 1,
1267
+ headers.length,
1268
+ ...rows.map((entry) => Array.isArray(entry) ? entry.length : 1),
1269
+ );
1270
+ const columns = Math.max(
1271
+ dataColumns,
1272
+ dashboard ? metrics.length * 2 : 1,
1273
+ );
1274
+ const lastColumn = columnLabel(columns);
1275
+ const dataLastColumn = columnLabel(dataColumns);
1276
+ let row = 1;
1277
+ if (operation.title) {
1278
+ output.push({ op: 'set_cell', sheet, cell: 'A1', value: String(operation.title) });
1279
+ if (columns > 1) output.push({ op: 'merge_cells', sheet, range: `A1:${lastColumn}1` });
1280
+ output.push({
1281
+ op: 'set_style',
1282
+ sheet,
1283
+ range: `A1:${lastColumn}1`,
1284
+ properties: {
1285
+ fontName: type.display,
1286
+ fontSize: Number(operation.titleSize) || format.title,
1287
+ bold: true,
1288
+ color: colors.onInverse,
1289
+ fillColor: colors.inverse,
1290
+ verticalAlignment: 'center',
1291
+ wrapText: true,
1292
+ },
1293
+ });
1294
+ row += 1;
1295
+ }
1296
+ if (operation.subtitle) {
1297
+ output.push({ op: 'set_cell', sheet, cell: `A${row}`, value: String(operation.subtitle) });
1298
+ if (columns > 1) output.push({ op: 'merge_cells', sheet, range: `A${row}:${lastColumn}${row}` });
1299
+ output.push({
1300
+ op: 'set_style',
1301
+ sheet,
1302
+ range: `A${row}:${lastColumn}${row}`,
1303
+ properties: {
1304
+ fontName: type.body,
1305
+ fontSize: format.body,
1306
+ italic: true,
1307
+ color: colors.muted,
1308
+ fillColor: colors.surface,
1309
+ wrapText: true,
1310
+ },
1311
+ });
1312
+ row += 2;
1313
+ } else if (operation.title) {
1314
+ row += 1;
1315
+ }
1316
+ if (metrics.length) {
1317
+ metrics.forEach((metric, index) => {
1318
+ const startColumn = (index * 2) + 1;
1319
+ const endColumn = startColumn + 1;
1320
+ const start = columnLabel(startColumn);
1321
+ const end = columnLabel(endColumn);
1322
+ const valueRow = row;
1323
+ const labelRow = row + 1;
1324
+ const detailRow = row + 2;
1325
+ const valueCell = `${start}${valueRow}`;
1326
+ if (metric?.formula) output.push({ op: 'set_formula', sheet, cell: valueCell, formula: String(metric.formula) });
1327
+ else output.push({ op: 'set_cell', sheet, cell: valueCell, value: metric?.value ?? '' });
1328
+ output.push({ op: 'merge_cells', sheet, range: `${start}${valueRow}:${end}${valueRow}` });
1329
+ output.push({ op: 'set_cell', sheet, cell: `${start}${labelRow}`, value: String(metric?.label || '') });
1330
+ output.push({ op: 'merge_cells', sheet, range: `${start}${labelRow}:${end}${labelRow}` });
1331
+ output.push({ op: 'set_cell', sheet, cell: `${start}${detailRow}`, value: String(metric?.detail || '') });
1332
+ output.push({ op: 'merge_cells', sheet, range: `${start}${detailRow}:${end}${detailRow}` });
1333
+ output.push({
1334
+ op: 'set_style',
1335
+ sheet,
1336
+ range: `${start}${valueRow}:${end}${valueRow}`,
1337
+ properties: {
1338
+ fontName: type.data,
1339
+ fontSize: 22,
1340
+ bold: true,
1341
+ color: index === 0 ? colors.onAccent : colors.ink,
1342
+ fillColor: index === 0 ? colors.accent : colors.surface,
1343
+ numberFormat: metric?.numberFormat || 'General',
1344
+ horizontalAlignment: 'center',
1345
+ verticalAlignment: 'center',
1346
+ },
1347
+ });
1348
+ output.push({
1349
+ op: 'set_style',
1350
+ sheet,
1351
+ range: `${start}${labelRow}:${end}${detailRow}`,
1352
+ properties: {
1353
+ fontName: type.body,
1354
+ fontSize: 9,
1355
+ bold: true,
1356
+ color: colors.muted,
1357
+ fillColor: colors.surface,
1358
+ horizontalAlignment: 'center',
1359
+ verticalAlignment: 'center',
1360
+ wrapText: true,
1361
+ },
1362
+ });
1363
+ });
1364
+ row += 4;
1365
+ }
1366
+ const insights = strings(operation.insights);
1367
+ if (insights.length) {
1368
+ output.push({ op: 'set_cell', sheet, cell: `A${row}`, value: insights.join(' • ') });
1369
+ if (columns > 1) output.push({ op: 'merge_cells', sheet, range: `A${row}:${lastColumn}${row}` });
1370
+ output.push({
1371
+ op: 'set_style',
1372
+ sheet,
1373
+ range: `A${row}:${lastColumn}${row}`,
1374
+ properties: {
1375
+ fontName: type.body,
1376
+ fontSize: format.body,
1377
+ bold: true,
1378
+ color: colors.ink,
1379
+ fillColor: colors.surface2,
1380
+ wrapText: true,
1381
+ },
1382
+ });
1383
+ row += 2;
1384
+ }
1385
+ const startRow = row;
1386
+ const values = headers.length ? [headers, ...rows] : rows;
1387
+ if (values.length) {
1388
+ const endRow = startRow + values.length - 1;
1389
+ output.push({
1390
+ op: 'set_range',
1391
+ sheet,
1392
+ range: `A${startRow}:${dataLastColumn}${endRow}`,
1393
+ values,
1394
+ });
1395
+ if (headers.length) {
1396
+ output.push({
1397
+ op: 'set_style',
1398
+ sheet,
1399
+ range: `A${startRow}:${dataLastColumn}${startRow}`,
1400
+ properties: {
1401
+ fontName: type.body,
1402
+ fontSize: format.heading,
1403
+ bold: true,
1404
+ color: colors.onAccent,
1405
+ fillColor: colors.accent,
1406
+ horizontalAlignment: 'center',
1407
+ verticalAlignment: 'center',
1408
+ wrapText: true,
1409
+ },
1410
+ });
1411
+ output.push({
1412
+ op: 'add_table',
1413
+ sheet,
1414
+ range: `A${startRow}:${dataLastColumn}${endRow}`,
1415
+ name: safeTableName(operation.tableName || `${sheet}Data`),
1416
+ style: operation.tableStyle || format.tableStyle,
1417
+ });
1418
+ output.push({ op: 'freeze_panes', sheet, row: startRow + 1, column: 1 });
1419
+ }
1420
+ output.push({
1421
+ op: 'set_style',
1422
+ sheet,
1423
+ range: `A${startRow + (headers.length ? 1 : 0)}:${dataLastColumn}${endRow}`,
1424
+ properties: {
1425
+ fontName: type.body,
1426
+ fontSize: format.body,
1427
+ color: colors.ink,
1428
+ verticalAlignment: 'center',
1429
+ },
1430
+ });
1431
+ if (plainObject(operation.columnFormats) && headers.length) {
1432
+ headers.forEach((header, index) => {
1433
+ const numberFormat = operation.columnFormats[header] || operation.columnFormats[columnLabel(index + 1)];
1434
+ if (!numberFormat || rows.length === 0) return;
1435
+ output.push({
1436
+ op: 'set_style',
1437
+ sheet,
1438
+ range: `${columnLabel(index + 1)}${startRow + 1}:${columnLabel(index + 1)}${endRow}`,
1439
+ properties: { numberFormat: String(numberFormat) },
1440
+ });
1441
+ });
1442
+ }
1443
+ if (plainObject(operation.chart)) {
1444
+ let chartRows = rows.length;
1445
+ while (chartRows > 0 && isExcelTotalRow(rows[chartRows - 1])) chartRows -= 1;
1446
+ const chartEndRow = startRow + (headers.length ? 1 : 0) + chartRows - 1;
1447
+ const chartRange = operation.chart.range
1448
+ || `A${startRow}:${dataLastColumn}${Math.max(startRow, chartEndRow)}`;
1449
+ output.push({
1450
+ op: 'add_chart',
1451
+ sheet,
1452
+ range: chartRange,
1453
+ chartType: operation.chart.type || 'column',
1454
+ title: operation.chart.title || '',
1455
+ left: Number(operation.chart.left) || (dashboard ? 390 : 520),
1456
+ top: Number(operation.chart.top) || (dashboard ? 220 : 40),
1457
+ width: Number(operation.chart.width) || (dashboard ? 400 : 480),
1458
+ height: Number(operation.chart.height) || (dashboard ? 260 : 280),
1459
+ });
1460
+ }
1461
+ }
1462
+ if (operation.source) {
1463
+ output.push({
1464
+ op: 'add_note',
1465
+ sheet,
1466
+ cell: 'A1',
1467
+ text: `Source: ${provenanceText(operation.source) || String(operation.source)}`,
1468
+ });
1469
+ }
1470
+ output.push({ op: 'autofit_range', sheet, range: `A:${dataLastColumn}` });
1471
+ const printColumns = plainObject(operation.chart) ? Math.max(columns, 14) : columns;
1472
+ output.push({
1473
+ op: 'set_page_setup',
1474
+ sheet,
1475
+ printArea: `A1:${columnLabel(printColumns)}${Math.max(row, startRow + values.length + 1, dashboard ? 24 : 1)}`,
1476
+ orientation: dashboard || plainObject(operation.chart) ? 'landscape' : 'portrait',
1477
+ fitToPagesWide: 1,
1478
+ fitToPagesTall: dashboard ? 1 : 0,
1479
+ centerHorizontally: true,
1480
+ });
1481
+ output.push({ op: 'set_sheet_view', sheet, showGridlines: false, zoom: dashboard ? 90 : 100 });
1482
+ return output;
1483
+ }
1484
+
1485
+ export function expandOfficeDesignOperations({
1486
+ format,
1487
+ backend = '',
1488
+ operations = [],
1489
+ design: request = {},
1490
+ library = null,
1491
+ created = false,
1492
+ snapshotVersion = 0,
1493
+ } = {}) {
1494
+ const normalizedFormat = String(format || '').toLowerCase();
1495
+ const design = resolveOfficeDesign(normalizedFormat, request, { library });
1496
+ const output = [];
1497
+ const semantic = [];
1498
+ let nextSlide = created && Number(snapshotVersion || 0) === 0 ? 1 : null;
1499
+ const docxState = { paragraph: 0, table: 0 };
1500
+ const layoutUsage = new Map();
1501
+ for (const operation of operations || []) {
1502
+ const bound = bindOfficeContent(operation, design.content);
1503
+ const contentOperation = bound.operation;
1504
+ const name = String(contentOperation?.op || '');
1505
+ if (normalizedFormat === 'pptx' && name === 'compose_slide') {
1506
+ const slide = Number(contentOperation.slide) || nextSlide;
1507
+ if (!slide) throw new Error('compose_slide requires slide for an existing presentation');
1508
+ const selectedLayout = selectPptxLayout(contentOperation, design, layoutUsage);
1509
+ const layout = selectedLayout?.layout || null;
1510
+ const composed = layout ? merge(layout.defaults || {}, contentOperation) : contentOperation;
1511
+ const backgroundSpec = pptxBackgroundSpec(composed, design, String(contentOperation.kind || 'content').toLowerCase(), slide);
1512
+ const plan = pptxSlidePlan(composed, String(contentOperation.kind || 'content').toLowerCase(), slide);
1513
+ const templateOperations = expandTemplatePptxSlide(composed, layout, slide);
1514
+ if (templateOperations) {
1515
+ output.push(...templateOperations);
1516
+ layoutUsage.set(layout.id, (layoutUsage.get(layout.id) || 0) + 1);
1517
+ } else {
1518
+ if (design.deck.templateMode === 'strict') {
1519
+ throw new Error(`compose_slide requires an approved native template layout for kind "${operation.kind}" in strict mode`);
1520
+ }
1521
+ output.push(...expandPptxSlide(composed, design, slide));
1522
+ }
1523
+ semantic.push({
1524
+ op: name,
1525
+ kind: String(contentOperation.kind || 'content'),
1526
+ slide,
1527
+ slideRole: backgroundSpec.slideRole,
1528
+ backgroundRole: backgroundSpec.backgroundRole,
1529
+ plan,
1530
+ renderMode: templateOperations ? 'native-template' : 'scratch',
1531
+ ...(bound.binding ? { contentBinding: bound.binding } : {}),
1532
+ ...(layout ? {
1533
+ layout: layout.id,
1534
+ variant: layout.variant || '',
1535
+ sourceSlide: Number(layout.sourceSlide) || 0,
1536
+ templateId: layout.templateId || '',
1537
+ selection: {
1538
+ score: selectedLayout.score,
1539
+ demand: selectedLayout.demand,
1540
+ fit: selectedLayout.fit,
1541
+ },
1542
+ } : {}),
1543
+ });
1544
+ if (nextSlide != null) nextSlide += 1;
1545
+ continue;
1546
+ }
1547
+ if (normalizedFormat === 'docx' && name === 'compose_document') {
1548
+ output.push(...expandDocxDocument(contentOperation, design, docxState, backend));
1549
+ semantic.push({
1550
+ op: name,
1551
+ sections: Array.isArray(contentOperation.sections) ? contentOperation.sections.length : 0,
1552
+ ...(bound.binding ? { contentBinding: bound.binding } : {}),
1553
+ });
1554
+ continue;
1555
+ }
1556
+ if (normalizedFormat === 'xlsx' && name === 'compose_sheet') {
1557
+ if (backend !== 'microsoft-office-com') throw new Error('compose_sheet requires Microsoft Excel');
1558
+ output.push(...expandXlsxSheet(contentOperation, design));
1559
+ semantic.push({
1560
+ op: name,
1561
+ sheet: String(contentOperation.sheet || 'Sheet1'),
1562
+ ...(bound.binding ? { contentBinding: bound.binding } : {}),
1563
+ });
1564
+ continue;
1565
+ }
1566
+ output.push(contentOperation);
1567
+ }
1568
+ const expandedOperations = normalizedFormat === 'pptx' && created && Number(snapshotVersion || 0) === 0
1569
+ ? coalesceCreatedPptxTemplateImports(output)
1570
+ : output;
1571
+ return {
1572
+ operations: expandedOperations,
1573
+ semantic,
1574
+ design: compactDesign(design),
1575
+ content: summarizeOfficeContentModel(design.content),
1576
+ };
1577
+ }
1578
+
1579
+ export function applyPdfDesign(blocks = [], designRequest = {}, { library = null } = {}) {
1580
+ const design = resolveOfficeDesign('pdf', designRequest, { library });
1581
+ const colors = design.tokens.colors;
1582
+ const type = design.tokens.typography;
1583
+ let headingIndex = 0;
1584
+ const styledBlocks = (blocks || []).map((block) => {
1585
+ const typeName = String(block?.type || 'paragraph').toLowerCase();
1586
+ if (typeName === 'heading') {
1587
+ headingIndex += 1;
1588
+ return {
1589
+ ...block,
1590
+ font: block.font || type.display,
1591
+ size: block.size || (headingIndex === 1 ? design.format.title : design.format.heading),
1592
+ color: block.color || (headingIndex === 1 ? colors.ink : colors.accent),
1593
+ after: block.after ?? (headingIndex === 1 ? 18 : 10),
1594
+ };
1595
+ }
1596
+ if (typeName === 'paragraph') {
1597
+ return {
1598
+ ...block,
1599
+ font: block.font || type.body,
1600
+ size: block.size || design.format.body,
1601
+ color: block.color || colors.ink,
1602
+ lineHeight: block.lineHeight || design.format.body * 1.5,
1603
+ after: block.after ?? 8,
1604
+ };
1605
+ }
1606
+ if (typeName === 'table') {
1607
+ return {
1608
+ ...block,
1609
+ font: block.font || type.data,
1610
+ color: block.color || colors.ink,
1611
+ headerFill: block.headerFill || colors.inverse,
1612
+ headerColor: block.headerColor || colors.onInverse,
1613
+ zebraFill: block.zebraFill || colors.surface,
1614
+ borderColor: block.borderColor || colors.surface2,
1615
+ };
1616
+ }
1617
+ return block;
1618
+ });
1619
+ return {
1620
+ blocks: styledBlocks,
1621
+ properties: {
1622
+ margin: design.format.margin,
1623
+ background: colors.canvas,
1624
+ fontName: type.body,
1625
+ ...compactDesign(design),
1626
+ },
1627
+ design: compactDesign(design),
1628
+ };
1629
+ }
1630
+
1631
+ function designIssue(code, path, message, severity = 'warning') {
1632
+ return { severity, code, path, message, source: 'design-review' };
1633
+ }
1634
+
1635
+ function pptxSlideBackgroundColor(slide) {
1636
+ const value = String(slide?.background?.color || '').replace(/^#/, '').toUpperCase();
1637
+ return /^[0-9A-F]{6}$/.test(value) ? value : '';
1638
+ }
1639
+
1640
+ function pptxExpectedBackgroundRole(slide, slides, deck) {
1641
+ if (slide.index === 1) return deck.roles.cover;
1642
+ if (slide.index === slides.length) return deck.roles.closing;
1643
+ if (deck.sectionSlides.includes(slide.index)) return deck.roles.section;
1644
+ return deck.roles.content;
1645
+ }
1646
+
1647
+ function reviewPptxTheme(slides, design, issues) {
1648
+ const deck = design.deck;
1649
+ if (!deck?.enforce || deck.backgroundMode === 'custom' || slides.length < 2) return;
1650
+ const backgrounds = slides.map(pptxSlideBackgroundColor);
1651
+ if (backgrounds.some((color) => !color)) return;
1652
+ const mismatches = [];
1653
+ slides.forEach((slide, index) => {
1654
+ const role = pptxExpectedBackgroundRole(slide, slides, deck);
1655
+ const expected = String(design.tokens.colors[role] || '').toUpperCase();
1656
+ if (expected && backgrounds[index] !== expected) {
1657
+ mismatches.push(`${slide.index}:${backgrounds[index]}→${role}`);
1658
+ }
1659
+ });
1660
+ if (mismatches.length) {
1661
+ issues.push(designIssue(
1662
+ 'theme_background_drift',
1663
+ '/',
1664
+ `Slide backgrounds violate the ${deck.backgroundMode} deck plan (${mismatches.join(', ')}).`,
1665
+ ));
1666
+ }
1667
+ const contentColors = slides
1668
+ .filter((slide) => slide.index !== 1 && slide.index !== slides.length && !deck.sectionSlides.includes(slide.index))
1669
+ .map(pptxSlideBackgroundColor);
1670
+ if (new Set(contentColors).size > 1) {
1671
+ issues.push(designIssue(
1672
+ 'theme_body_backgrounds',
1673
+ '/',
1674
+ 'Content slides use multiple background colors instead of one dominant canvas.',
1675
+ ));
1676
+ }
1677
+ }
1678
+
1679
+ function normalizedShapeSignature(slide) {
1680
+ return (slide.shapes || [])
1681
+ .filter((shape) => String(shape.text || '').trim())
1682
+ .map((shape) => [
1683
+ Number(shape.type) || 0,
1684
+ Math.round((Number(shape.left) || 0) / 24),
1685
+ Math.round((Number(shape.top) || 0) / 24),
1686
+ Math.round((Number(shape.width) || 0) / 24),
1687
+ Math.round((Number(shape.height) || 0) / 24),
1688
+ ].join(':'))
1689
+ .sort()
1690
+ .join('|');
1691
+ }
1692
+
1693
+ function reviewPptx(document, design) {
1694
+ const issues = [];
1695
+ const slides = Array.isArray(document?.slides) ? document.slides : [];
1696
+ const plansBySlide = new Map((design.slidePlans || []).map((plan) => [Number(plan.slide), plan]));
1697
+ reviewPptxTheme(slides, design, issues);
1698
+ let cardGridSlides = 0;
1699
+ const signatures = new Map();
1700
+ let nativeEvidenceSlides = 0;
1701
+ for (const slide of slides) {
1702
+ const shapes = Array.isArray(slide.shapes) ? slide.shapes : [];
1703
+ const textShapes = shapes.filter((shape) => String(shape.text || '').trim());
1704
+ const pictures = shapes.filter((shape) => Number(shape.type) === 13);
1705
+ const richVisuals = shapes.filter((shape) => shape.chart || shape.table || shape.group);
1706
+ if (pictures.length || shapes.some((shape) => shape.chart || shape.table)) nativeEvidenceSlides += 1;
1707
+ const nonTextShapes = shapes.filter((shape) => !String(shape.text || '').trim() && !shape.placeholder);
1708
+ const typographicVisual = textShapes.some((shape) => Number(shape.font?.size) >= 42)
1709
+ || textShapes.filter((shape) => Number(shape.font?.size) >= 34).length >= 2;
1710
+ const semanticVisual = String(plansBySlide.get(Number(slide.index))?.visualType || '');
1711
+ const purposefulDiagram = ['comparison', 'diagram', 'matrix', 'process'].includes(semanticVisual)
1712
+ && nonTextShapes.length > 0;
1713
+ const contentSlide = slides.length <= 2 || (slide.index !== 1 && slide.index !== slides.length);
1714
+ if (
1715
+ contentSlide
1716
+ && textShapes.length
1717
+ && pictures.length === 0
1718
+ && richVisuals.length === 0
1719
+ && nonTextShapes.length < 2
1720
+ && !typographicVisual
1721
+ && !purposefulDiagram
1722
+ && !design.review.allowTextOnly
1723
+ ) {
1724
+ issues.push(designIssue(
1725
+ 'meaningful_visual_missing',
1726
+ `/slide[${slide.index}]`,
1727
+ 'Content slide has no image, chart, table, group, or purposeful diagram.',
1728
+ ));
1729
+ }
1730
+ const textLength = textShapes.reduce((total, shape) => total + String(shape.text || '').length, 0);
1731
+ if (textLength > 650) {
1732
+ issues.push(designIssue(
1733
+ 'excessive_slide_text',
1734
+ `/slide[${slide.index}]`,
1735
+ `Slide contains ${textLength} characters; split or visualize the content.`,
1736
+ ));
1737
+ }
1738
+ const ornamental = nonTextShapes.filter((shape) => {
1739
+ const width = Number(shape.width) || 0;
1740
+ const height = Number(shape.height) || 0;
1741
+ return (width <= 14 && height >= 180) || (height <= 7 && width >= 320);
1742
+ });
1743
+ if (ornamental.length && !design.review.allowDecorativeLines) {
1744
+ issues.push(designIssue(
1745
+ 'decorative_stripe',
1746
+ `/slide[${slide.index}]`,
1747
+ 'Thin decorative stripe or rule resembles generic AI slide ornamentation.',
1748
+ ));
1749
+ }
1750
+ const rectangularText = textShapes.filter((shape) => Number(shape.type) === 1);
1751
+ const sizeGroups = new Map();
1752
+ for (const shape of rectangularText) {
1753
+ const key = `${Math.round((Number(shape.width) || 0) / 12)}:${Math.round((Number(shape.height) || 0) / 12)}`;
1754
+ sizeGroups.set(key, (sizeGroups.get(key) || 0) + 1);
1755
+ }
1756
+ if ([...sizeGroups.values()].some((count) => count >= 3)) cardGridSlides += 1;
1757
+ if (contentSlide) {
1758
+ const signature = normalizedShapeSignature(slide);
1759
+ if (signature) signatures.set(signature, (signatures.get(signature) || 0) + 1);
1760
+ }
1761
+ }
1762
+ const contentCount = slides.length > 2 ? slides.length - 2 : slides.length;
1763
+ const repeated = Math.max(0, ...signatures.values());
1764
+ if (contentCount >= 4 && repeated / contentCount >= 0.75 && !design.review.allowRepetition) {
1765
+ issues.push(designIssue(
1766
+ 'repetitive_composition',
1767
+ '/',
1768
+ `${repeated} of ${contentCount} content slides repeat the same composition.`,
1769
+ ));
1770
+ }
1771
+ if (cardGridSlides >= 2 && !design.review.allowRepetition) {
1772
+ issues.push(designIssue(
1773
+ 'card_grid_overuse',
1774
+ '/',
1775
+ `${cardGridSlides} slides use repeated same-size text cards; vary the visual structure.`,
1776
+ ));
1777
+ }
1778
+ const requiredNativeEvidence = Math.max(1, Math.ceil(contentCount / 3));
1779
+ if (
1780
+ slides.length >= 5
1781
+ && nativeEvidenceSlides < requiredNativeEvidence
1782
+ && !design.review.allowSyntheticVisuals
1783
+ ) {
1784
+ issues.push(designIssue(
1785
+ 'native_evidence_too_weak',
1786
+ '/',
1787
+ `Deck uses native image, chart, or table evidence on ${nativeEvidenceSlides} slide(s); at least ${requiredNativeEvidence} are required.`,
1788
+ ));
1789
+ }
1790
+ return issues;
1791
+ }
1792
+
1793
+ function reviewDocx(document) {
1794
+ const issues = [];
1795
+ const paragraphs = Array.isArray(document?.paragraphs) ? document.paragraphs : [];
1796
+ const content = paragraphs.filter((paragraph) => String(paragraph.text || '').trim());
1797
+ const headings = content.filter((paragraph) => /title|heading|제목/i.test(String(paragraph.style || '')));
1798
+ if (content.length >= 8 && headings.length === 0) {
1799
+ issues.push(designIssue(
1800
+ 'heading_hierarchy_missing',
1801
+ '/body',
1802
+ 'Document has substantial content but no visible title or heading hierarchy.',
1803
+ ));
1804
+ }
1805
+ for (const paragraph of content) {
1806
+ if (String(paragraph.text || '').length > 900) {
1807
+ issues.push(designIssue(
1808
+ 'dense_paragraph',
1809
+ paragraph.path || '/body',
1810
+ 'Paragraph is too dense for fast document scanning.',
1811
+ ));
1812
+ }
1813
+ }
1814
+ return issues;
1815
+ }
1816
+
1817
+ function reviewXlsx(document) {
1818
+ const issues = [];
1819
+ const sheets = Array.isArray(document?.sheets) ? document.sheets : [];
1820
+ for (const sheet of sheets) {
1821
+ const cells = Array.isArray(sheet.cells) ? sheet.cells : [];
1822
+ if (cells.length < 8) continue;
1823
+ const styled = cells.filter((cell) => cell.style && Object.keys(cell.style).length);
1824
+ if (styled.length === 0) {
1825
+ issues.push(designIssue(
1826
+ 'worksheet_hierarchy_missing',
1827
+ sheet.path || `/sheet[${sheet.name || ''}]`,
1828
+ 'Data sheet has no styled title, header, table, or visual hierarchy.',
1829
+ ));
1830
+ }
1831
+ }
1832
+ return issues;
1833
+ }
1834
+
1835
+ export function reviewPptxVisualCritique({ critique = [], pageCount = 0 } = {}) {
1836
+ const total = Math.max(0, Number(pageCount) || 0);
1837
+ const issues = [];
1838
+ const entries = [];
1839
+ const bySlide = new Map();
1840
+ for (const raw of Array.isArray(critique) ? critique : []) {
1841
+ if (!plainObject(raw)) continue;
1842
+ const slide = Number(raw.slide);
1843
+ if (!Number.isInteger(slide) || slide < 1 || slide > total || bySlide.has(slide)) {
1844
+ issues.push({
1845
+ severity: 'warning',
1846
+ code: 'visual_critique_invalid_slide',
1847
+ path: '/',
1848
+ message: `Visual critique has an invalid or duplicate slide index: ${raw.slide}`,
1849
+ source: 'visual-critique',
1850
+ });
1851
+ continue;
1852
+ }
1853
+ const scores = Object.fromEntries(PPTX_CRITIQUE_AXES.map((axis) => [axis, Number(raw[axis])]));
1854
+ const note = String(raw.note || '').trim();
1855
+ const fixes = strings(raw.fixes);
1856
+ const verdict = String(raw.verdict || '').toLowerCase();
1857
+ const validScores = PPTX_CRITIQUE_AXES.every((axis) => Number.isInteger(scores[axis]) && scores[axis] >= 1 && scores[axis] <= 5);
1858
+ const entry = { slide, verdict, ...scores, note, fixes };
1859
+ entries.push(entry);
1860
+ bySlide.set(slide, entry);
1861
+ if (!validScores || note.length < 40) {
1862
+ issues.push({
1863
+ severity: 'warning',
1864
+ code: 'visual_critique_incomplete',
1865
+ path: `/slide[${slide}]`,
1866
+ message: 'Visual critique requires five integer scores from 1-5 and a slide-specific note of at least 40 characters.',
1867
+ source: 'visual-critique',
1868
+ });
1869
+ } else if (verdict !== 'pass' || fixes.length || PPTX_CRITIQUE_AXES.some((axis) => scores[axis] < 4)) {
1870
+ issues.push({
1871
+ severity: 'warning',
1872
+ code: 'visual_critique_needs_polish',
1873
+ path: `/slide[${slide}]`,
1874
+ message: `Slide ${slide} still needs polish before finalization.`,
1875
+ source: 'visual-critique',
1876
+ });
1877
+ }
1878
+ }
1879
+ for (let slide = 1; slide <= total; slide += 1) {
1880
+ if (!bySlide.has(slide)) {
1881
+ issues.push({
1882
+ severity: 'warning',
1883
+ code: 'visual_critique_missing_slide',
1884
+ path: `/slide[${slide}]`,
1885
+ message: `Slide ${slide} has no visual critique.`,
1886
+ source: 'visual-critique',
1887
+ });
1888
+ }
1889
+ }
1890
+ const notes = entries.map((entry) => entry.note.toLowerCase()).filter(Boolean);
1891
+ if (total > 1 && notes.length === total && new Set(notes).size !== total) {
1892
+ issues.push({
1893
+ severity: 'warning',
1894
+ code: 'visual_critique_repeated_note',
1895
+ path: '/',
1896
+ message: 'Each slide needs a distinct visual critique note.',
1897
+ source: 'visual-critique',
1898
+ });
1899
+ }
1900
+ return {
1901
+ ok: total > 0 && issues.length === 0,
1902
+ status: total > 0 && issues.length === 0 ? 'pass' : 'needs-polish',
1903
+ axes: [...PPTX_CRITIQUE_AXES],
1904
+ pageCount: total,
1905
+ entries,
1906
+ issues,
1907
+ };
1908
+ }
1909
+
1910
+ export function pptxVisualReviewAcknowledged({
1911
+ reviewed = false,
1912
+ providedToken = '',
1913
+ expectedToken = '',
1914
+ renderedVersion = null,
1915
+ snapshotVersion = 0,
1916
+ critiqueOk = false,
1917
+ } = {}) {
1918
+ return reviewed === true
1919
+ && Boolean(expectedToken)
1920
+ && String(providedToken || '') === String(expectedToken)
1921
+ && Number(renderedVersion) === Number(snapshotVersion || 0)
1922
+ && critiqueOk === true;
1923
+ }
1924
+
1925
+ export function reviewOfficeDesign({
1926
+ format,
1927
+ document,
1928
+ design: request = {},
1929
+ library = null,
1930
+ auditProfile = '',
1931
+ } = {}) {
1932
+ const normalizedFormat = String(format || '').toLowerCase();
1933
+ const design = resolveOfficeDesign(normalizedFormat, request, { library });
1934
+ if (!design.review.required) {
1935
+ return {
1936
+ ok: true,
1937
+ status: 'skipped',
1938
+ profile: design.profile,
1939
+ issues: [],
1940
+ requiresVisualInspection: false,
1941
+ };
1942
+ }
1943
+ const structureIssues = reviewOfficeStructure({
1944
+ format: normalizedFormat,
1945
+ document,
1946
+ auditProfile,
1947
+ });
1948
+ const issues = normalizedFormat === 'pptx'
1949
+ ? [...reviewPptx(document, design), ...structureIssues]
1950
+ : structureIssues;
1951
+ return {
1952
+ ok: issues.length === 0,
1953
+ status: issues.length ? 'needs-polish' : 'pass',
1954
+ profile: design.profile,
1955
+ issues,
1956
+ requiresVisualInspection: !['csv', 'tsv'].includes(normalizedFormat),
1957
+ modelReview: [
1958
+ 'Inspect every rendered page, not only lint counts.',
1959
+ 'Verify the reading path states the conclusion, evidence, and requested decision before supporting detail.',
1960
+ 'Trace material numbers from source through calculation to the displayed claim; reject decorative or unsupported data.',
1961
+ 'Reject generic palette, decorative stripes, repeated cards, weak hierarchy, and interchangeable layouts.',
1962
+ 'Confirm the design is specific to the subject, audience, and intended action.',
1963
+ 'Refine the current composition before adding decoration.',
1964
+ ],
1965
+ };
1966
+ }