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,486 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ import { executeOfficeTool, resetOfficeSessionsForTest } from './index.mjs';
7
+
8
+ function toolValue(result, label) {
9
+ const text = result?.content?.find((entry) => entry.type === 'text')?.text || '';
10
+ if (result?.isError) throw new Error(`${label}: ${text}`);
11
+ return JSON.parse(text);
12
+ }
13
+
14
+ async function office(args, cwd, label = args.action) {
15
+ const raw = await executeOfficeTool(args, { cwd });
16
+ return { raw, value: toolValue(raw, label) };
17
+ }
18
+
19
+ async function persistRenderImages(raw, value, directory, prefix) {
20
+ const imageParts = raw.content.filter((entry) => entry.type === 'image');
21
+ const metadata = value?.preview?.images || value?.images || [];
22
+ const files = [];
23
+ for (let index = 0; index < imageParts.length; index += 1) {
24
+ const pages = Array.isArray(metadata[index]?.pages) && metadata[index].pages.length
25
+ ? `pages-${metadata[index].pages.join('-')}`
26
+ : `page-${metadata[index]?.page || index + 1}`;
27
+ const path = join(directory, `${prefix}-${pages}.png`);
28
+ await writeFile(path, Buffer.from(imageParts[index].source.data, 'base64'));
29
+ files.push(path);
30
+ }
31
+ return files;
32
+ }
33
+
34
+ function contentModel() {
35
+ return {
36
+ packageId: 'july-executive-review',
37
+ audience: '7월 경영회의',
38
+ objective: '실적을 검토하고 성장·고객 유지 투자를 결정',
39
+ decision: '성장 가속 0.9억원과 고객 유지 0.9억원 승인',
40
+ period: '2026-07',
41
+ facts: [
42
+ { id: 'revenue', label: '7월 매출', value: 5660, unit: '백만원', numberFormat: '#,##0', source: { document: '01-dashboard.xlsx', target: 'Source!B4' } },
43
+ { id: 'operating-profit', label: '7월 영업이익', value: 802, unit: '백만원', numberFormat: '#,##0', source: { document: '01-dashboard.xlsx', target: 'Source!C4' } },
44
+ { id: 'operating-margin', label: '영업이익률', value: 0.1417, unit: '%', numberFormat: '0.0%', source: { document: '01-dashboard.xlsx', target: 'Calculation!B4' } },
45
+ { id: 'churn', label: '고객 이탈률', value: 0.026, unit: '%', numberFormat: '0.0%', source: { document: '01-dashboard.xlsx', target: 'Source!E4' } },
46
+ { id: 'nps', label: 'NPS', value: 55, unit: '점', numberFormat: '0', source: { document: '01-dashboard.xlsx', target: 'Source!F4' } },
47
+ { id: 'growth-investment', label: '성장 가속 투자', value: 90, unit: '백만원', numberFormat: '#,##0', source: { document: '01-dashboard.xlsx', target: 'Calculation!B7' } },
48
+ { id: 'retention-investment', label: '고객 유지 투자', value: 90, unit: '백만원', numberFormat: '#,##0', source: { document: '01-dashboard.xlsx', target: 'Calculation!B8' } },
49
+ ],
50
+ claims: [
51
+ {
52
+ id: 'growth-case',
53
+ text: '매출 5,660백만원과 영업이익 802백만원이 성장 투자의 여력을 만들었습니다',
54
+ implication: '성장 가속 투자를 이번 달부터 집행할 수 있습니다',
55
+ factIds: ['revenue', 'operating-profit', 'operating-margin'],
56
+ },
57
+ {
58
+ id: 'retention-risk',
59
+ text: '이탈률은 2.6%로 개선됐지만 고객 유지 투자는 계속 필요합니다',
60
+ implication: 'NPS 55를 유지하면서 이탈률을 2.4% 아래로 낮춰야 합니다',
61
+ factIds: ['churn', 'nps'],
62
+ },
63
+ {
64
+ id: 'investment-decision',
65
+ text: '성장 0.9억원과 고객 유지 0.9억원을 함께 승인해야 합니다',
66
+ implication: '총 1.8억원을 두 트랙으로 즉시 배분합니다',
67
+ factIds: ['growth-investment', 'retention-investment'],
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ function design(content, profile = 'data') {
74
+ return {
75
+ profile,
76
+ audience: content.audience,
77
+ intent: content.objective,
78
+ tone: 'executive',
79
+ density: 'balanced',
80
+ signature: 'source-bound July operating review',
81
+ content,
82
+ };
83
+ }
84
+
85
+ async function createWorkbook(directory, content) {
86
+ const path = join(directory, '01-dashboard.xlsx');
87
+ const operations = [
88
+ { op: 'rename_sheet', sheet: 'Sheet1', name: 'Source' },
89
+ {
90
+ op: 'set_range',
91
+ sheet: 'Source',
92
+ range: 'A1:F4',
93
+ values: [
94
+ ['월', '매출(백만원)', '영업이익(백만원)', '활성고객', '이탈률', 'NPS'],
95
+ ['5월', 5000, 650, 1040, 0.031, 49],
96
+ ['6월', 5300, 735, 1060, 0.029, 52],
97
+ ['7월', 5660, 802, 1082, 0.026, 55],
98
+ ],
99
+ },
100
+ ...['B2', 'C2', 'D2', 'E2', 'F2', 'B3', 'C3', 'D3', 'E3', 'F3', 'B4', 'C4', 'D4', 'E4', 'F4']
101
+ .map((cell) => ({ op: 'add_note', sheet: 'Source', cell, text: 'Source: 2026년 7월 benchmark brief input' })),
102
+ { op: 'set_style', sheet: 'Source', range: 'A1:F1', properties: { bold: true, color: 'FFFFFF', fillColor: '183028', horizontalAlignment: 'center' } },
103
+ { op: 'set_style', sheet: 'Source', range: 'E2:E4', properties: { numberFormat: '0.0%' } },
104
+ { op: 'autofit_range', sheet: 'Source', range: 'A:F' },
105
+ { op: 'add_sheet', name: 'Calculation' },
106
+ {
107
+ op: 'set_range',
108
+ sheet: 'Calculation',
109
+ range: 'A1:B9',
110
+ values: [
111
+ ['지표', '값'],
112
+ ['7월 매출', null],
113
+ ['7월 영업이익', null],
114
+ ['영업이익률', null],
115
+ ['고객 이탈률', null],
116
+ ['NPS', null],
117
+ ['성장 가속 투자', 90],
118
+ ['고객 유지 투자', 90],
119
+ ['총 투자', null],
120
+ ],
121
+ },
122
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B2', formula: '=Source!B4' },
123
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B3', formula: '=Source!C4' },
124
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B4', formula: '=B3/B2' },
125
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B5', formula: '=Source!E4' },
126
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B6', formula: '=Source!F4' },
127
+ { op: 'set_formula', sheet: 'Calculation', cell: 'B9', formula: '=SUM(B7:B8)' },
128
+ { op: 'add_note', sheet: 'Calculation', cell: 'B7', text: 'Source: 경영회의 투자 scenario input' },
129
+ { op: 'add_note', sheet: 'Calculation', cell: 'B8', text: 'Source: 경영회의 투자 scenario input' },
130
+ { op: 'set_style', sheet: 'Calculation', range: 'A1:B1', properties: { bold: true, color: 'FFFFFF', fillColor: '183028' } },
131
+ { op: 'set_style', sheet: 'Calculation', range: 'B4:B5', properties: { numberFormat: '0.0%' } },
132
+ { op: 'autofit_range', sheet: 'Calculation', range: 'A:B' },
133
+ { op: 'add_sheet', name: 'Checks' },
134
+ {
135
+ op: 'set_range',
136
+ sheet: 'Checks',
137
+ range: 'A1:C4',
138
+ values: [
139
+ ['검사', '상태', '차이'],
140
+ ['매출 원장 일치', null, null],
141
+ ['이익률 계산 일치', null, null],
142
+ ['투자 합계 1.8억원', null, null],
143
+ ],
144
+ },
145
+ { op: 'set_formula', sheet: 'Checks', cell: 'B2', formula: '=IF(Calculation!B2=Source!B4,"PASS","FAIL")' },
146
+ { op: 'set_formula', sheet: 'Checks', cell: 'C2', formula: '=Calculation!B2-Source!B4' },
147
+ { op: 'set_formula', sheet: 'Checks', cell: 'B3', formula: '=IF(ABS(Calculation!B4-Source!C4/Source!B4)<0.000001,"PASS","FAIL")' },
148
+ { op: 'set_formula', sheet: 'Checks', cell: 'C3', formula: '=Calculation!B4-Source!C4/Source!B4' },
149
+ { op: 'set_formula', sheet: 'Checks', cell: 'B4', formula: '=IF(Calculation!B9=180,"PASS","FAIL")' },
150
+ { op: 'set_formula', sheet: 'Checks', cell: 'C4', formula: '=Calculation!B9-180' },
151
+ { op: 'set_style', sheet: 'Checks', range: 'A1:C1', properties: { bold: true, color: 'FFFFFF', fillColor: '183028' } },
152
+ { op: 'autofit_range', sheet: 'Checks', range: 'A:C' },
153
+ { op: 'add_sheet', name: 'Dashboard' },
154
+ {
155
+ op: 'compose_sheet',
156
+ sheet: 'Dashboard',
157
+ kind: 'dashboard',
158
+ title: '7월 경영회의 실적 대시보드',
159
+ subtitle: '성장 여력은 확보했습니다. 다음 결정은 성장 0.9억원 + 고객 유지 0.9억원입니다.',
160
+ claimId: 'investment-decision',
161
+ source: { document: '01-dashboard.xlsx', target: 'Source!A1:F4' },
162
+ headers: ['월', '매출', '영업이익', '영업이익률', '이탈률', 'NPS'],
163
+ rows: [
164
+ ['5월', 5000, 650, 0.13, 0.031, 49],
165
+ ['6월', 5300, 735, 0.1387, 0.029, 52],
166
+ ['7월', 5660, 802, 0.1417, 0.026, 55],
167
+ ['합계', 15960, 2187, 0.137, '', ''],
168
+ ],
169
+ metrics: [
170
+ { factId: 'revenue', formula: '=Calculation!B2' },
171
+ { factId: 'operating-profit', formula: '=Calculation!B3' },
172
+ { factId: 'operating-margin', formula: '=Calculation!B4' },
173
+ { factId: 'churn', formula: '=Calculation!B5' },
174
+ ],
175
+ insights: [
176
+ '매출과 영업이익률이 3개월 연속 개선',
177
+ 'NPS 55 유지',
178
+ '총 1.8억원을 성장과 고객 유지에 균등 배분',
179
+ ],
180
+ columnFormats: {
181
+ 매출: '#,##0',
182
+ 영업이익: '#,##0',
183
+ 영업이익률: '0.0%',
184
+ 이탈률: '0.0%',
185
+ NPS: '0',
186
+ },
187
+ chart: { type: 'column', title: '월별 매출 추이', range: 'A10:B13' },
188
+ },
189
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'B11', formula: '=Source!B2' },
190
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'C11', formula: '=Source!C2' },
191
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'D11', formula: '=C11/B11' },
192
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'E11', formula: '=Source!E2' },
193
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'F11', formula: '=Source!F2' },
194
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'B12', formula: '=Source!B3' },
195
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'C12', formula: '=Source!C3' },
196
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'D12', formula: '=C12/B12' },
197
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'E12', formula: '=Source!E3' },
198
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'F12', formula: '=Source!F3' },
199
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'B13', formula: '=Source!B4' },
200
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'C13', formula: '=Source!C4' },
201
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'D13', formula: '=C13/B13' },
202
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'E13', formula: '=Source!E4' },
203
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'F13', formula: '=Source!F4' },
204
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'B14', formula: '=SUM(B11:B13)' },
205
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'C14', formula: '=SUM(C11:C13)' },
206
+ { op: 'set_formula', sheet: 'Dashboard', cell: 'D14', formula: '=C14/B14' },
207
+ { op: 'set_sheet_visibility', sheet: 'Source', visibility: 'hidden' },
208
+ { op: 'set_sheet_visibility', sheet: 'Calculation', visibility: 'hidden' },
209
+ { op: 'set_sheet_visibility', sheet: 'Checks', visibility: 'hidden' },
210
+ ];
211
+ const created = await office({
212
+ action: 'create',
213
+ path,
214
+ format: 'xlsx',
215
+ mode: 'background',
216
+ design: design(content, 'data'),
217
+ operations,
218
+ }, directory, 'create workbook');
219
+ const qa = await office({
220
+ action: 'qa',
221
+ session: created.value.session,
222
+ output: join(directory, '01-dashboard-preview.pdf'),
223
+ task: '7월 경영회의 실적 대시보드',
224
+ auditProfile: 'financial-model',
225
+ }, directory, 'review workbook');
226
+ const images = await persistRenderImages(qa.raw, qa.value, directory, '01-dashboard-preview');
227
+ const validation = await office({
228
+ action: 'validate',
229
+ session: created.value.session,
230
+ auditProfile: 'financial-model',
231
+ assertions: [
232
+ { kind: 'cell-value', sheet: 'Dashboard', cell: 'B13', equals: 5660 },
233
+ { kind: 'cell-value', sheet: 'Dashboard', cell: 'C13', equals: 802 },
234
+ { kind: 'cell-value', sheet: 'Calculation', cell: 'B9', equals: 180 },
235
+ { kind: 'no-formula-errors' },
236
+ ],
237
+ }, directory, 'validate workbook');
238
+ await office({ action: 'close', session: created.value.session }, directory, 'close workbook');
239
+ return { path, created: created.value, qa: qa.value, validation: validation.value, images };
240
+ }
241
+
242
+ async function createDocument(directory, content) {
243
+ const path = join(directory, '02-decision-brief.docx');
244
+ const created = await office({
245
+ action: 'create',
246
+ path,
247
+ format: 'docx',
248
+ mode: 'background',
249
+ design: design(content, 'editorial'),
250
+ operations: [{
251
+ op: 'compose_document',
252
+ title: '7월 경영회의 의사결정 브리프',
253
+ subtitle: '성장 여력은 확보했습니다. 고객 유지와 함께 투자해야 합니다.',
254
+ claimId: 'investment-decision',
255
+ summary: '권고: 성장 가속 0.9억원과 고객 유지 0.9억원을 함께 승인합니다. 총 1.8억원을 30일 실행계획으로 관리합니다.',
256
+ meta: ['2026년 7월', '경영회의용', '단위: 백만원'],
257
+ sections: [
258
+ {
259
+ heading: '1. 결론과 요청사항',
260
+ paragraphs: [
261
+ '7월 매출은 5,660백만원, 영업이익은 802백만원으로 개선 흐름을 이어갔습니다. 영업이익률 14.2%는 성장 투자 여력을 뒷받침합니다.',
262
+ '동시에 고객 이탈률 2.6%와 NPS 55를 고려하면 성장만 단독 집행하기보다 고객 유지 투자를 병행하는 편이 안전합니다.',
263
+ ],
264
+ bullets: [
265
+ '승인 요청: 성장 가속 90백만원',
266
+ '승인 요청: 고객 유지 90백만원',
267
+ '운영 원칙: 월말에 성과 gate를 다시 판정',
268
+ ],
269
+ },
270
+ {
271
+ heading: '2. 핵심 실적',
272
+ table: [
273
+ ['지표', '7월 실적', '판정'],
274
+ ['매출', '5,660백만원', '성장 여력 확보'],
275
+ ['영업이익', '802백만원', '수익성 개선'],
276
+ ['영업이익률', '14.2%', '계획 상회'],
277
+ ['고객 이탈률', '2.6%', '추가 개선 필요'],
278
+ ['NPS', '55', '유지'],
279
+ ],
280
+ },
281
+ {
282
+ heading: '3. 투자 release / stop gate',
283
+ paragraphs: ['두 트랙 모두 승인하되, 다음 월말에 정량 기준으로 계속 집행 여부를 다시 결정합니다.'],
284
+ table: [
285
+ ['트랙', 'Release', 'Stop'],
286
+ ['성장 가속 0.9억원', '매출 성장률과 영업이익률 유지', '영업이익률 13.5% 미만'],
287
+ ['고객 유지 0.9억원', '이탈률 2.4% 이하 경로 확인', '이탈률 반등 또는 NPS 52 미만'],
288
+ ],
289
+ },
290
+ {
291
+ heading: '4. 30일 실행계획',
292
+ bullets: [
293
+ '1주차: 성장 채널과 유지 캠페인별 owner·예산 확정',
294
+ '2주차: cohort별 전환·이탈 leading indicator 점검',
295
+ '3주차: 저효율 집행분 중단 및 고효율 채널 재배분',
296
+ '4주차: 경영회의에 release / stop 재판정 보고',
297
+ ],
298
+ },
299
+ ],
300
+ footer: 'Source: 01-dashboard.xlsx · Source→Calculation→Dashboard',
301
+ pageNumbers: true,
302
+ }],
303
+ }, directory, 'create document');
304
+ const qa = await office({
305
+ action: 'qa',
306
+ session: created.value.session,
307
+ output: join(directory, '02-decision-brief-preview.pdf'),
308
+ task: '7월 경영회의 의사결정 브리프',
309
+ }, directory, 'review document');
310
+ const images = await persistRenderImages(qa.raw, qa.value, directory, '02-decision-brief-preview');
311
+ const validation = await office({ action: 'validate', session: created.value.session }, directory, 'validate document');
312
+ await office({ action: 'close', session: created.value.session }, directory, 'close document');
313
+ return { path, created: created.value, qa: qa.value, validation: validation.value, images };
314
+ }
315
+
316
+ async function createPresentation(directory, content) {
317
+ const path = join(directory, '03-executive-deck.pptx');
318
+ const operations = [
319
+ {
320
+ op: 'compose_slide',
321
+ kind: 'cover',
322
+ title: '7월 실적은 성장 투자의 여력을 증명했습니다',
323
+ subtitle: '이제 성장과 고객 유지에 각각 0.9억원을 배분할 결정이 필요합니다',
324
+ eyebrow: 'JULY EXECUTIVE REVIEW',
325
+ meta: ['2026년 7월', '경영회의'],
326
+ source: '01-dashboard.xlsx#Dashboard',
327
+ },
328
+ {
329
+ op: 'compose_slide',
330
+ kind: 'statement',
331
+ claimId: 'growth-case',
332
+ title: '매출 5,660백만원이 성장 투자의 여력을 만들었습니다',
333
+ subtitle: '영업이익 802백만원 · 영업이익률 14.2%',
334
+ metrics: [{ factId: 'revenue' }],
335
+ },
336
+ {
337
+ op: 'compose_slide',
338
+ kind: 'chart',
339
+ claimId: 'growth-case',
340
+ title: '매출과 수익성이 3개월 연속 함께 개선됐습니다',
341
+ body: ['7월 매출 5,660백만원', '7월 영업이익 802백만원', '영업이익률 14.2%'],
342
+ chart: {
343
+ type: 'column',
344
+ title: '월별 매출(백만원)',
345
+ categories: ['5월', '6월', '7월'],
346
+ series: [{ name: '매출', values: [5000, 5300, 5660] }],
347
+ },
348
+ },
349
+ {
350
+ op: 'compose_slide',
351
+ kind: 'table',
352
+ claimId: 'retention-risk',
353
+ title: '성장과 고객 유지 중 하나를 포기할 이유가 없습니다',
354
+ subtitle: '두 트랙은 서로 다른 위험을 줄입니다',
355
+ table: [
356
+ ['트랙', 'Release 기준', 'Stop 기준'],
357
+ ['성장 0.9억원', '영업이익률 13.5% 이상', '13.5% 미만'],
358
+ ['고객 유지 0.9억원', '이탈률 2.4% 이하 경로', 'NPS 52 미만'],
359
+ ],
360
+ },
361
+ {
362
+ op: 'compose_slide',
363
+ kind: 'process',
364
+ title: '30일 안에 두 트랙의 성과를 다시 판정합니다',
365
+ steps: [
366
+ { title: 'Owner 확정', detail: '채널·cohort별 책임 지정' },
367
+ { title: 'Leading 지표', detail: '전환·이탈 조기 점검' },
368
+ { title: '재배분', detail: '저효율 집행 중단' },
369
+ { title: 'Gate 판정', detail: '월말 release / stop' },
370
+ ],
371
+ source: '02-decision-brief.docx#4. 30일 실행계획',
372
+ },
373
+ {
374
+ op: 'compose_slide',
375
+ kind: 'closing',
376
+ claimId: 'investment-decision',
377
+ title: '오늘 두 트랙에 총 1.8억원 투자를 승인해 주십시오',
378
+ subtitle: '성장 0.9억원 · 고객 유지 0.9억원 · 월말 gate 재판정',
379
+ visualText: '1.8억',
380
+ visualLabel: '승인 요청',
381
+ },
382
+ ];
383
+ const created = await office({
384
+ action: 'create',
385
+ path,
386
+ format: 'pptx',
387
+ mode: 'background',
388
+ design: {
389
+ ...design(content, 'data'),
390
+ deck: {
391
+ backgroundMode: 'sandwich',
392
+ templateMode: 'scratch',
393
+ requireSlidePlan: true,
394
+ },
395
+ },
396
+ operations,
397
+ }, directory, 'create presentation');
398
+ const qa = await office({
399
+ action: 'qa',
400
+ session: created.value.session,
401
+ output: join(directory, '03-executive-deck-preview.pdf'),
402
+ task: '7월 경영회의 6장 발표',
403
+ auditProfile: 'model-backed-deck',
404
+ }, directory, 'review presentation');
405
+ const images = await persistRenderImages(qa.raw, qa.value, directory, '03-executive-deck-preview');
406
+ const validation = await office({
407
+ action: 'validate',
408
+ session: created.value.session,
409
+ auditProfile: 'model-backed-deck',
410
+ }, directory, 'validate presentation');
411
+ await office({ action: 'close', session: created.value.session }, directory, 'close presentation');
412
+ return { path, created: created.value, qa: qa.value, validation: validation.value, images };
413
+ }
414
+
415
+ function compactResult(entry) {
416
+ const issues = entry.qa.issuesAfter || [];
417
+ const postSaveBlocking = Array.isArray(entry.validation.postSaveGate?.blocking)
418
+ ? entry.validation.postSaveGate.blocking
419
+ : [];
420
+ const criticalIssues = [
421
+ ...issues.filter((issue) => issue.severity === 'error').map((issue) => issue.code),
422
+ ...postSaveBlocking.filter((issue) => issue.severity === 'error').map((issue) => issue.code),
423
+ ];
424
+ return {
425
+ path: entry.path,
426
+ images: entry.images,
427
+ pageCount: entry.qa.preview?.pageCount || 0,
428
+ qaOk: entry.qa.ok,
429
+ validationOk: entry.validation.ok,
430
+ issueCodes: issues.map((issue) => issue.code),
431
+ criticalIssues: [...new Set(criticalIssues)],
432
+ contentFingerprint: entry.created.batch?.content?.fingerprint
433
+ || entry.created.batch?.design?.content?.fingerprint
434
+ || entry.created.design?.content?.fingerprint
435
+ || '',
436
+ postSaveGate: entry.validation.postSaveGate,
437
+ };
438
+ }
439
+
440
+ export async function runOfficeQualityLiveBenchmark({ output = '' } = {}) {
441
+ if (process.platform !== 'win32') throw new Error('Office quality live benchmark requires Windows and Microsoft Office');
442
+ const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
443
+ const directory = resolve(output || join(tmpdir(), `mixdog-office-quality-${timestamp}`));
444
+ await mkdir(directory, { recursive: true });
445
+ const content = contentModel();
446
+ const results = {};
447
+ try {
448
+ results.xlsx = compactResult(await createWorkbook(directory, content));
449
+ results.docx = compactResult(await createDocument(directory, content));
450
+ results.pptx = compactResult(await createPresentation(directory, content));
451
+ } finally {
452
+ resetOfficeSessionsForTest();
453
+ }
454
+ const fingerprints = Object.values(results).map((entry) => entry.contentFingerprint).filter(Boolean);
455
+ const crossAppConsistent = fingerprints.length === 3 && new Set(fingerprints).size === 1;
456
+ const criticalCount = Object.values(results).reduce((total, entry) => total + entry.criticalIssues.length, 0);
457
+ const report = {
458
+ version: 1,
459
+ createdAt: new Date().toISOString(),
460
+ directory,
461
+ request: '7월 경영회의용 Excel 대시보드, Word 의사결정 브리프, PowerPoint 6장을 같은 데이터에서 생성',
462
+ content: {
463
+ packageId: content.packageId,
464
+ decision: content.decision,
465
+ facts: Object.fromEntries(content.facts.map((fact) => [fact.id, fact.value])),
466
+ },
467
+ crossAppConsistent,
468
+ criticalCount,
469
+ automatedPass: crossAppConsistent
470
+ && criticalCount === 0
471
+ && Object.values(results).every((entry) => entry.qaOk && entry.validationOk),
472
+ results,
473
+ nextAction: 'Inspect every PNG, score content/design/layout/form/request fidelity, then apply one targeted polish batch per failing file.',
474
+ };
475
+ await writeFile(join(directory, 'quality-report.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
476
+ return report;
477
+ }
478
+
479
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
480
+ const outputIndex = process.argv.indexOf('--output');
481
+ const output = outputIndex >= 0 ? process.argv[outputIndex + 1] : '';
482
+ const report = await runOfficeQualityLiveBenchmark({ output });
483
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
484
+ if (!report.automatedPass) process.exit(1);
485
+ }
486
+
@@ -0,0 +1,136 @@
1
+ import { extname } from 'node:path';
2
+
3
+ const CRITICAL_CODES = new Set([
4
+ 'blank_page',
5
+ 'broken_chart',
6
+ 'empty_chart',
7
+ 'formula_error',
8
+ 'formula_error_truncated',
9
+ 'missing_relationship',
10
+ 'package_corrupt',
11
+ 'render_failed',
12
+ 'text_outside_slide',
13
+ ]);
14
+
15
+ const POLISH_GUIDANCE = Object.freeze({
16
+ blank_page: 'Remove the accidental page or rebalance preceding content so the page has a clear purpose.',
17
+ sparse_page: 'Rebalance sections, tables, and page breaks instead of padding the page with decoration.',
18
+ broken_chart: 'Rebuild the native chart from source-bound data, save, close, and verify the series after reopen.',
19
+ empty_chart: 'Populate the chart embedded workbook from the cited source range, then verify seriesCount after reopen.',
20
+ formula_error: 'Trace the formula to its source cells, fix the calculation, recalculate, and verify the displayed value.',
21
+ chart_includes_total_row: 'Separate comparison rows from total or subtotal rows and narrow the chart source range.',
22
+ worksheet_print_too_small: 'Recompose the sheet for one-page-wide reading; move support data off the dashboard if needed.',
23
+ worksheet_print_fit_missing: 'Set a deliberate print area, landscape orientation when useful, and one-page-wide fitting.',
24
+ heading_hierarchy_missing: 'Create a clear title and heading hierarchy that matches the document reading path.',
25
+ orphan_heading: 'Keep the heading with the paragraph or table it introduces.',
26
+ short_table_split: 'Keep the short table together or move it intact to the next page.',
27
+ shape_overlap: 'Move or resize the reported shapes while preserving a consistent alignment grid.',
28
+ text_overflow: 'Shorten the copy or enlarge the text area before reducing type size.',
29
+ small_font: 'Reduce content density or split the slide; keep presentation body text at least 12 pt.',
30
+ meaningful_visual_missing: 'Replace generic text blocks with a chart, table, image, or subject-specific diagram that proves the claim.',
31
+ native_evidence_too_weak: 'Add source-bound native evidence to the slides carrying material claims.',
32
+ repetitive_composition: 'Choose a different semantic layout that matches this slide’s evidence and role.',
33
+ card_grid_overuse: 'Replace repeated cards with one dominant hierarchy, comparison, process, chart, or table.',
34
+ number_without_source: 'Add a source note that identifies the workbook cell, range, or external document.',
35
+ generic_takeaway: 'Rewrite the title as a specific conclusion or decision, not a topic label.',
36
+ });
37
+
38
+ export function resolveOfficeRenderOutput(path) {
39
+ const value = String(path || '');
40
+ if (extname(value).toLowerCase() === '.pdf') return value;
41
+ return value.replace(/\.[^./\\]+$/u, '') + '.pdf';
42
+ }
43
+
44
+ export function normalizeOfficeReviewIssues(entries = []) {
45
+ const seen = new Set();
46
+ const output = [];
47
+ for (const raw of entries || []) {
48
+ if (!raw || typeof raw !== 'object') continue;
49
+ const issue = {
50
+ ...raw,
51
+ severity: CRITICAL_CODES.has(String(raw.code || ''))
52
+ ? 'error'
53
+ : String(raw.severity || 'warning'),
54
+ };
55
+ const key = `${issue.severity}\0${issue.code}\0${issue.path}\0${issue.message}`;
56
+ if (seen.has(key)) continue;
57
+ seen.add(key);
58
+ output.push(issue);
59
+ }
60
+ return output;
61
+ }
62
+
63
+ export function buildOfficePolishPlan({
64
+ format = '',
65
+ issues = [],
66
+ } = {}) {
67
+ const normalized = normalizeOfficeReviewIssues(issues);
68
+ const targets = new Map();
69
+ for (const issue of normalized) {
70
+ const path = String(issue.path || '/');
71
+ const current = targets.get(path) || {
72
+ path,
73
+ severity: 'warning',
74
+ codes: [],
75
+ actions: [],
76
+ };
77
+ if (issue.severity === 'error') current.severity = 'error';
78
+ if (!current.codes.includes(issue.code)) current.codes.push(issue.code);
79
+ const guidance = POLISH_GUIDANCE[issue.code]
80
+ || `Correct ${issue.code} at ${path}, then render and inspect the affected page again.`;
81
+ if (!current.actions.includes(guidance)) current.actions.push(guidance);
82
+ targets.set(path, current);
83
+ }
84
+ const ordered = [...targets.values()].sort((left, right) => (
85
+ Number(right.severity === 'error') - Number(left.severity === 'error')
86
+ || left.path.localeCompare(right.path)
87
+ ));
88
+ return {
89
+ format: String(format || '').toLowerCase(),
90
+ status: ordered.length ? 'needs-polish' : 'pass',
91
+ targetCount: ordered.length,
92
+ criticalCount: ordered.filter((entry) => entry.severity === 'error').length,
93
+ targets: ordered,
94
+ nextAction: ordered.length
95
+ ? 'Edit only the reported targets in one atomic batch, render the changed pages, then run QA again.'
96
+ : 'No targeted polish remains.',
97
+ };
98
+ }
99
+
100
+ export function evaluateOfficeSubmissionGate({
101
+ issues = [],
102
+ persisted = null,
103
+ visualCoverage = null,
104
+ } = {}) {
105
+ const normalized = normalizeOfficeReviewIssues(issues);
106
+ const blocking = normalized.filter((issue) => issue.severity === 'error');
107
+ if (persisted === false) {
108
+ blocking.push({
109
+ severity: 'error',
110
+ code: 'post_save_reopen_missing',
111
+ path: '/',
112
+ message: 'The saved Office document was not reopened, so persistence is unproven.',
113
+ source: 'post-save-gate',
114
+ });
115
+ }
116
+ if (visualCoverage && visualCoverage.complete !== true) {
117
+ blocking.push({
118
+ severity: 'error',
119
+ code: 'visual_coverage_incomplete',
120
+ path: '/',
121
+ message: 'Not every rendered page or slide was reviewed.',
122
+ source: 'render-review',
123
+ });
124
+ }
125
+ return {
126
+ ok: blocking.length === 0,
127
+ persisted,
128
+ criticalCount: blocking.length,
129
+ blocking,
130
+ };
131
+ }
132
+
133
+ export function officeCriticalIssueCodes() {
134
+ return [...CRITICAL_CODES].sort();
135
+ }
136
+