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,591 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { basename, dirname, extname, resolve } from 'node:path';
3
+ import {
4
+ PDFDocument,
5
+ PDFArray,
6
+ PDFDict,
7
+ PDFHexString,
8
+ PDFName,
9
+ PDFString,
10
+ StandardFonts,
11
+ degrees,
12
+ rgb,
13
+ } from 'pdf-lib';
14
+ import { inspectPdfBuffer } from '../attachments/pdf-extract.mjs';
15
+ import { ocrPdf } from './pdf-analysis.mjs';
16
+
17
+ function color(value = '') {
18
+ const hex = String(value || '000000').replace(/^#/, '');
19
+ if (!/^[0-9a-f]{6}$/i.test(hex)) throw new Error(`Invalid PDF color: ${value}`);
20
+ return rgb(
21
+ Number.parseInt(hex.slice(0, 2), 16) / 255,
22
+ Number.parseInt(hex.slice(2, 4), 16) / 255,
23
+ Number.parseInt(hex.slice(4, 6), 16) / 255,
24
+ );
25
+ }
26
+
27
+ function selectedPages(document, operation) {
28
+ const count = document.getPageCount();
29
+ const pages = Array.isArray(operation.pages) && operation.pages.length
30
+ ? operation.pages
31
+ : operation.page
32
+ ? [operation.page]
33
+ : Array.from({ length: count }, (_, index) => index + 1);
34
+ return pages.map((page) => {
35
+ const index = Number(page) - 1;
36
+ if (!Number.isInteger(index) || index < 0 || index >= count) throw new Error(`PDF page out of range: ${page}`);
37
+ return { page: document.getPage(index), index };
38
+ });
39
+ }
40
+
41
+ function pageSize(properties = {}) {
42
+ const named = String(properties.pageSize || 'a4').toLowerCase();
43
+ if (named === 'letter') return [612, 792];
44
+ if (named === 'legal') return [612, 1008];
45
+ if (Array.isArray(properties.pageSize) && properties.pageSize.length === 2) {
46
+ return properties.pageSize.map(Number);
47
+ }
48
+ return [595.28, 841.89];
49
+ }
50
+
51
+ function fieldWidgets(field, document) {
52
+ try {
53
+ return field.acroField.getWidgets().map((widget) => {
54
+ const rectangle = widget.getRectangle();
55
+ const pageRef = widget.P();
56
+ const page = document.getPages().findIndex((entry) => entry.ref === pageRef) + 1;
57
+ return {
58
+ page,
59
+ x: Number(rectangle.x),
60
+ y: Number(rectangle.y),
61
+ width: Number(rectangle.width),
62
+ height: Number(rectangle.height),
63
+ };
64
+ });
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ function pdfAttachments(document) {
71
+ try {
72
+ const names = document.catalog.lookup(PDFName.of('Names'), PDFDict);
73
+ const embedded = names?.lookup(PDFName.of('EmbeddedFiles'), PDFDict);
74
+ const entries = embedded?.lookup(PDFName.of('Names'), PDFArray);
75
+ if (!entries) return [];
76
+ const output = [];
77
+ for (let index = 0; index + 1 < entries.size(); index += 2) {
78
+ const nameObject = entries.lookup(index, PDFString, PDFHexString);
79
+ const spec = entries.lookup(index + 1, PDFDict);
80
+ const description = spec?.lookupMaybe?.(PDFName.of('Desc'), PDFString, PDFHexString);
81
+ output.push({
82
+ path: `/attachments[${output.length + 1}]`,
83
+ index: output.length + 1,
84
+ name: nameObject?.decodeText?.() || '',
85
+ description: description?.decodeText?.() || '',
86
+ });
87
+ }
88
+ return output;
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ function fieldValue(field) {
95
+ const type = field.constructor?.name || '';
96
+ try {
97
+ if (type.includes('CheckBox')) return field.isChecked();
98
+ if (type.includes('RadioGroup') || type.includes('Dropdown') || type.includes('OptionList')) return field.getSelected();
99
+ if (type.includes('TextField')) return field.getText() || '';
100
+ } catch {}
101
+ return null;
102
+ }
103
+
104
+ function rectanglesOverlap(left, right) {
105
+ return left.x < right.x + right.width
106
+ && left.x + left.width > right.x
107
+ && left.y < right.y + right.height
108
+ && left.y + left.height > right.y;
109
+ }
110
+
111
+ export function lintPdfFormFields(fields = [], pages = []) {
112
+ const issues = [];
113
+ const normalized = fields.map((field, index) => ({
114
+ index: index + 1,
115
+ name: String(field.name || ''),
116
+ type: String(field.type || 'text').toLowerCase(),
117
+ page: Math.max(1, Number(field.page) || 1),
118
+ x: Number(field.x),
119
+ y: Number(field.y),
120
+ width: Number(field.width),
121
+ height: Number(field.height),
122
+ }));
123
+ const names = new Set();
124
+ for (const field of normalized) {
125
+ const size = pages[field.page - 1] || [595.28, 841.89];
126
+ if (!field.name) issues.push({ severity: 'error', code: 'missing_field_name', path: `/field[${field.index}]`, message: 'Form field name is required.' });
127
+ if (names.has(field.name)) issues.push({ severity: 'error', code: 'duplicate_field_name', path: `/field[${field.index}]`, message: `Duplicate form field name: ${field.name}` });
128
+ names.add(field.name);
129
+ if (![field.x, field.y, field.width, field.height].every(Number.isFinite) || field.width <= 0 || field.height <= 0) {
130
+ issues.push({ severity: 'error', code: 'invalid_field_box', path: `/field[${field.index}]`, message: 'Form field box must have finite positive dimensions.' });
131
+ } else if (field.x < 0 || field.y < 0 || field.x + field.width > size[0] || field.y + field.height > size[1]) {
132
+ issues.push({ severity: 'error', code: 'field_outside_page', path: `/field[${field.index}]`, message: `Form field is outside page ${field.page}.` });
133
+ }
134
+ }
135
+ for (let left = 0; left < normalized.length; left += 1) {
136
+ for (let right = left + 1; right < normalized.length; right += 1) {
137
+ if (normalized[left].page === normalized[right].page && rectanglesOverlap(normalized[left], normalized[right])) {
138
+ issues.push({
139
+ severity: 'warning',
140
+ code: 'overlapping_form_fields',
141
+ path: `/field[${normalized[left].index}]`,
142
+ message: `Form fields ${normalized[left].name || normalized[left].index} and ${normalized[right].name || normalized[right].index} overlap.`,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ return { ok: !issues.some((issue) => issue.severity === 'error'), fields: normalized, issueCount: issues.length, issues };
148
+ }
149
+
150
+ async function embedPdfFont(document, properties = {}) {
151
+ if (!properties.fontPath) return await document.embedFont(StandardFonts.Helvetica);
152
+ const fontkit = (await import('@pdf-lib/fontkit')).default;
153
+ document.registerFontkit(fontkit);
154
+ return await document.embedFont(await readFile(resolve(String(properties.fontPath))), { subset: true });
155
+ }
156
+
157
+ function wrapText(text, font, size, width) {
158
+ const words = String(text || '').split(/\s+/);
159
+ const lines = [];
160
+ let line = '';
161
+ for (const word of words) {
162
+ const candidate = line ? `${line} ${word}` : word;
163
+ if (!line || font.widthOfTextAtSize(candidate, size) <= width) line = candidate;
164
+ else {
165
+ lines.push(line);
166
+ line = word;
167
+ }
168
+ }
169
+ if (line) lines.push(line);
170
+ return lines.length ? lines : [''];
171
+ }
172
+
173
+ async function addFormField(document, field) {
174
+ const page = document.getPage(Math.max(1, Number(field.page) || 1) - 1);
175
+ const form = document.getForm();
176
+ const name = String(field.name);
177
+ const options = {
178
+ x: Number(field.x),
179
+ y: Number(field.y),
180
+ width: Number(field.width),
181
+ height: Number(field.height),
182
+ borderWidth: Number(field.borderWidth ?? 1),
183
+ textColor: color(field.textColor || '000000'),
184
+ borderColor: color(field.borderColor || '666666'),
185
+ backgroundColor: color(field.backgroundColor || 'FFFFFF'),
186
+ };
187
+ const type = String(field.type || 'text').toLowerCase();
188
+ if (type === 'checkbox') {
189
+ const control = form.createCheckBox(name);
190
+ control.addToPage(page, options);
191
+ if (field.value) control.check();
192
+ } else if (type === 'dropdown') {
193
+ const control = form.createDropdown(name);
194
+ control.addOptions((field.options || []).map(String));
195
+ control.addToPage(page, options);
196
+ if (field.value != null) control.select(String(field.value));
197
+ } else if (type === 'radio') {
198
+ const control = form.createRadioGroup(name);
199
+ for (const option of field.options || []) {
200
+ control.addOptionToPage(String(option.value ?? option), page, {
201
+ ...options,
202
+ x: Number(option.x ?? field.x),
203
+ y: Number(option.y ?? field.y),
204
+ });
205
+ }
206
+ if (field.value != null) control.select(String(field.value));
207
+ } else {
208
+ const control = form.createTextField(name);
209
+ control.addToPage(page, options);
210
+ if (field.multiline) control.enableMultiline();
211
+ if (field.value != null) control.setText(String(field.value));
212
+ }
213
+ }
214
+
215
+ export async function createPdf(path, {
216
+ blocks = [],
217
+ fields = [],
218
+ properties = {},
219
+ } = {}) {
220
+ const document = await PDFDocument.create();
221
+ const size = pageSize(properties);
222
+ const margin = Number(properties.margin ?? 54);
223
+ const font = await embedPdfFont(document, properties);
224
+ const preparePage = (entry) => {
225
+ if (properties.background) {
226
+ entry.drawRectangle({
227
+ x: 0,
228
+ y: 0,
229
+ width: entry.getWidth(),
230
+ height: entry.getHeight(),
231
+ color: color(properties.background),
232
+ });
233
+ }
234
+ return entry;
235
+ };
236
+ let page = preparePage(document.addPage(size));
237
+ let y = page.getHeight() - margin;
238
+ const newPage = () => {
239
+ page = preparePage(document.addPage(size));
240
+ y = page.getHeight() - margin;
241
+ };
242
+ for (const block of blocks || []) {
243
+ const type = String(block.type || 'paragraph').toLowerCase();
244
+ if (type === 'pagebreak') {
245
+ newPage();
246
+ continue;
247
+ }
248
+ if (type === 'image') {
249
+ const imagePath = resolve(dirname(path), String(block.path || ''));
250
+ const data = await readFile(imagePath);
251
+ const image = /\.png$/i.test(imagePath) ? await document.embedPng(data) : await document.embedJpg(data);
252
+ const width = Number(block.width || Math.min(image.width, page.getWidth() - (margin * 2)));
253
+ const height = Number(block.height || (image.height * width / image.width));
254
+ if (y - height < margin) newPage();
255
+ page.drawImage(image, { x: Number(block.x ?? margin), y: Number(block.y ?? y - height), width, height });
256
+ y -= height + Number(block.after ?? 12);
257
+ continue;
258
+ }
259
+ if (type === 'table') {
260
+ const rows = Array.isArray(block.rows) ? block.rows : [];
261
+ const columns = Math.max(1, ...rows.map((row) => row.length));
262
+ const width = Number(block.width || page.getWidth() - (margin * 2));
263
+ const cellWidth = width / columns;
264
+ const rowHeight = Number(block.rowHeight || 24);
265
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
266
+ const row = rows[rowIndex];
267
+ if (y - rowHeight < margin) newPage();
268
+ row.forEach((value, column) => {
269
+ const x = margin + (column * cellWidth);
270
+ const fill = rowIndex === 0
271
+ ? block.headerFill
272
+ : rowIndex % 2 === 0
273
+ ? block.zebraFill
274
+ : '';
275
+ page.drawRectangle({
276
+ x,
277
+ y: y - rowHeight,
278
+ width: cellWidth,
279
+ height: rowHeight,
280
+ ...(fill ? { color: color(fill) } : {}),
281
+ borderWidth: 0.5,
282
+ borderColor: color(block.borderColor || '999999'),
283
+ });
284
+ page.drawText(String(value ?? ''), {
285
+ x: x + 4,
286
+ y: y - rowHeight + 7,
287
+ size: Number(block.fontSize || 9),
288
+ font,
289
+ color: color(rowIndex === 0 ? block.headerColor || block.color : block.color),
290
+ });
291
+ });
292
+ y -= rowHeight;
293
+ }
294
+ y -= Number(block.after ?? 12);
295
+ continue;
296
+ }
297
+ const heading = type === 'heading';
298
+ const fontSize = Number(block.size || (heading ? 20 : 11));
299
+ const lineHeight = Number(block.lineHeight || fontSize * 1.35);
300
+ const lines = wrapText(block.text, font, fontSize, page.getWidth() - (margin * 2));
301
+ for (const line of lines) {
302
+ if (y - lineHeight < margin) newPage();
303
+ page.drawText(line, { x: Number(block.x ?? margin), y: y - fontSize, size: fontSize, font, color: color(block.color) });
304
+ y -= lineHeight;
305
+ }
306
+ y -= Number(block.after ?? (heading ? 10 : 6));
307
+ }
308
+ if (properties.title != null) document.setTitle(String(properties.title));
309
+ if (properties.author != null) document.setAuthor(String(properties.author));
310
+ if (properties.subject != null) document.setSubject(String(properties.subject));
311
+ if (properties.keywords != null) document.setKeywords(Array.isArray(properties.keywords) ? properties.keywords.map(String) : [String(properties.keywords)]);
312
+ const formCheck = lintPdfFormFields(fields, document.getPages().map((entry) => [entry.getWidth(), entry.getHeight()]));
313
+ if (!formCheck.ok) throw new Error(`PDF form layout is invalid: ${formCheck.issues.filter((issue) => issue.severity === 'error').map((issue) => issue.message).join(' ')}`);
314
+ for (const field of fields || []) await addFormField(document, field);
315
+ await writeFile(path, await document.save({ useObjectStreams: true, addDefaultPage: false }));
316
+ return { ok: true, path, pages: document.getPageCount(), form: formCheck };
317
+ }
318
+
319
+ export async function snapshotPdf(path, options = {}) {
320
+ const maxChars = Math.max(1_000, Number(options.maxChars) || 30_000);
321
+ const buffer = await readFile(path);
322
+ const structure = await PDFDocument.load(buffer, {
323
+ ignoreEncryption: false,
324
+ updateMetadata: false,
325
+ });
326
+ const pageCount = structure.getPageCount();
327
+ const offset = options.paged ? Math.max(0, Number(options.offset) || 0) : 0;
328
+ const limit = options.paged ? Math.max(1, Number(options.limit) || 20) : pageCount;
329
+ const selected = Array.isArray(options.pages) && options.pages.length
330
+ ? options.pages.map(Number)
331
+ : Array.from({ length: Math.max(0, Math.min(limit, pageCount - offset)) }, (_, index) => offset + index + 1);
332
+ const from = selected.length ? Math.min(...selected) : 1;
333
+ const to = selected.length ? Math.max(...selected) : 1;
334
+ const result = await inspectPdfBuffer(buffer, {
335
+ extractText: true,
336
+ maxPages: Math.max(500, pageCount),
337
+ maxOutputBytes: maxChars,
338
+ pageRange: { from, to },
339
+ });
340
+ const pages = [];
341
+ const regex = /--- Page (\d+) ---\n([\s\S]*?)(?=\n\n--- Page \d+ ---|$)/g;
342
+ let match;
343
+ while ((match = regex.exec(result.text || ''))) {
344
+ const index = Number(match[1]);
345
+ if (selected.includes(index)) pages.push({ path: `/page[${index}]`, index, text: match[2] });
346
+ }
347
+ const fields = structure.getForm().getFields().map((field, index) => ({
348
+ path: `/field[${index + 1}]`,
349
+ index: index + 1,
350
+ name: field.getName(),
351
+ type: field.constructor?.name || '',
352
+ value: fieldValue(field),
353
+ widgets: fieldWidgets(field, structure),
354
+ }));
355
+ const attachments = pdfAttachments(structure);
356
+ const likelyScannedPages = pages.filter((page) => page.text.includes('(no extractable text on this page)')).map((page) => page.index);
357
+ return {
358
+ format: 'pdf',
359
+ ...result,
360
+ pageCount,
361
+ pages,
362
+ fields,
363
+ fieldCount: fields.length,
364
+ attachmentCount: attachments.length,
365
+ attachments,
366
+ metadata: {
367
+ title: structure.getTitle() || '',
368
+ author: structure.getAuthor() || '',
369
+ subject: structure.getSubject() || '',
370
+ keywords: structure.getKeywords() || '',
371
+ },
372
+ likelyScannedPages,
373
+ ocrRequired: likelyScannedPages.length > 0,
374
+ encrypted: false,
375
+ ...(options.paged ? {
376
+ pagination: {
377
+ unit: 'page',
378
+ offset,
379
+ limit,
380
+ returned: pages.length,
381
+ total: pageCount,
382
+ nextOffset: !options.pages?.length && offset + pages.length < pageCount ? offset + pages.length : null,
383
+ },
384
+ } : {}),
385
+ };
386
+ }
387
+
388
+ export async function applyPdfBatch(path, operations, context = {}) {
389
+ let document = await PDFDocument.load(await readFile(path), {
390
+ ignoreEncryption: false,
391
+ updateMetadata: false,
392
+ });
393
+ const results = [];
394
+ for (const operation of operations) {
395
+ switch (operation.op) {
396
+ case 'add_text':
397
+ case 'watermark': {
398
+ const font = await embedPdfFont(document, operation);
399
+ for (const { page } of selectedPages(document, operation)) {
400
+ const size = Number(operation.size ?? (operation.op === 'watermark' ? 48 : 12));
401
+ const opacity = Number(operation.opacity ?? (operation.op === 'watermark' ? 0.25 : 1));
402
+ const x = Number(operation.x ?? 36);
403
+ const y = Number(operation.y ?? (operation.op === 'watermark' ? page.getHeight() / 2 : 36));
404
+ page.drawText(String(operation.text || ''), {
405
+ x,
406
+ y,
407
+ size,
408
+ font,
409
+ color: color(operation.color),
410
+ opacity,
411
+ rotate: degrees(Number(operation.rotation ?? (operation.op === 'watermark' ? 45 : 0))),
412
+ });
413
+ }
414
+ results.push({ op: operation.op, changed: true, fontEmbedded: Boolean(operation.fontPath) });
415
+ break;
416
+ }
417
+ case 'stamp_image': {
418
+ const imagePath = resolve(dirname(path), String(operation.path || ''));
419
+ const data = await readFile(imagePath);
420
+ const image = /\.png$/i.test(imagePath) ? await document.embedPng(data) : await document.embedJpg(data);
421
+ for (const { page } of selectedPages(document, operation)) {
422
+ const width = Number(operation.width || image.width);
423
+ const height = Number(operation.height || (image.height * width / image.width));
424
+ page.drawImage(image, {
425
+ x: Number(operation.x || 0),
426
+ y: Number(operation.y || 0),
427
+ width,
428
+ height,
429
+ opacity: Number(operation.opacity ?? 1),
430
+ });
431
+ }
432
+ results.push({ op: operation.op, changed: true, image: imagePath });
433
+ break;
434
+ }
435
+ case 'ocr_pages': {
436
+ await writeFile(path, await document.save({ useObjectStreams: true, addDefaultPage: false }));
437
+ const result = await ocrPdf(path, operation, context);
438
+ document = await PDFDocument.load(await readFile(path), {
439
+ ignoreEncryption: false,
440
+ updateMetadata: false,
441
+ });
442
+ results.push(result);
443
+ break;
444
+ }
445
+ case 'rotate_pages': {
446
+ for (const { page } of selectedPages(document, operation)) page.setRotation(degrees(Number(operation.rotation ?? 90)));
447
+ results.push({ op: operation.op, changed: true });
448
+ break;
449
+ }
450
+ case 'delete_pages': {
451
+ const indexes = selectedPages(document, operation).map(({ index }) => index).sort((a, b) => b - a);
452
+ for (const index of indexes) document.removePage(index);
453
+ results.push({ op: operation.op, changed: indexes.length > 0, count: indexes.length });
454
+ break;
455
+ }
456
+ case 'extract_pages': {
457
+ const next = await PDFDocument.create();
458
+ const indexes = selectedPages(document, operation).map(({ index }) => index);
459
+ const copied = await next.copyPages(document, indexes);
460
+ copied.forEach((page) => next.addPage(page));
461
+ document = next;
462
+ results.push({ op: operation.op, changed: true, count: indexes.length });
463
+ break;
464
+ }
465
+ case 'fill_form': {
466
+ const form = document.getForm();
467
+ for (const [name, value] of Object.entries(operation.values || {})) {
468
+ const field = form.getField(name);
469
+ const type = field.constructor?.name || '';
470
+ if (type.includes('CheckBox')) {
471
+ value ? field.check() : field.uncheck();
472
+ } else if (type.includes('Dropdown') || type.includes('OptionList') || type.includes('RadioGroup')) {
473
+ field.select(String(value));
474
+ } else {
475
+ field.setText(String(value ?? ''));
476
+ }
477
+ }
478
+ if (operation.flatten) form.flatten();
479
+ results.push({ op: operation.op, changed: true });
480
+ break;
481
+ }
482
+ case 'add_form_field': {
483
+ const check = lintPdfFormFields([operation], document.getPages().map((entry) => [entry.getWidth(), entry.getHeight()]));
484
+ if (!check.ok) throw new Error(check.issues.map((issue) => issue.message).join(' '));
485
+ await addFormField(document, operation);
486
+ results.push({ op: operation.op, changed: true, name: operation.name });
487
+ break;
488
+ }
489
+ case 'flatten_form': {
490
+ document.getForm().flatten();
491
+ results.push({ op: operation.op, changed: true });
492
+ break;
493
+ }
494
+ case 'add_attachment': {
495
+ const attachmentPath = resolve(dirname(path), String(operation.path || ''));
496
+ await document.attach(await readFile(attachmentPath), String(operation.name || basename(attachmentPath)), {
497
+ mimeType: String(operation.mimeType || 'application/octet-stream'),
498
+ description: String(operation.description || ''),
499
+ });
500
+ results.push({ op: operation.op, changed: true, name: String(operation.name || basename(attachmentPath)) });
501
+ break;
502
+ }
503
+ case 'compress': {
504
+ results.push({ op: operation.op, changed: true, method: 'object-streams' });
505
+ break;
506
+ }
507
+ case 'merge_pdf': {
508
+ const otherPath = resolve(dirname(path), String(operation.path || ''));
509
+ const other = await PDFDocument.load(await readFile(otherPath));
510
+ const copied = await document.copyPages(other, other.getPageIndices());
511
+ copied.forEach((page) => document.addPage(page));
512
+ results.push({ op: operation.op, changed: true, pagesAdded: copied.length });
513
+ break;
514
+ }
515
+ case 'set_metadata': {
516
+ const props = operation.properties || {};
517
+ if (props.title !== undefined) document.setTitle(String(props.title));
518
+ if (props.author !== undefined) document.setAuthor(String(props.author));
519
+ if (props.subject !== undefined) document.setSubject(String(props.subject));
520
+ if (props.keywords !== undefined) document.setKeywords(Array.isArray(props.keywords) ? props.keywords.map(String) : [String(props.keywords)]);
521
+ results.push({ op: operation.op, changed: true });
522
+ break;
523
+ }
524
+ case 'move_page': {
525
+ const from = Number(operation.page) - 1;
526
+ const to = Number(operation.index) - 1;
527
+ const count = document.getPageCount();
528
+ if (!Number.isInteger(from) || from < 0 || from >= count) throw new Error(`PDF page out of range: ${operation.page}`);
529
+ if (!Number.isInteger(to) || to < 0 || to >= count) throw new Error(`PDF destination page out of range: ${operation.index}`);
530
+ const order = document.getPageIndices();
531
+ const [moved] = order.splice(from, 1);
532
+ order.splice(to, 0, moved);
533
+ const next = await PDFDocument.create();
534
+ const copied = await next.copyPages(document, order);
535
+ copied.forEach((page) => next.addPage(page));
536
+ document = next;
537
+ results.push({ op: operation.op, changed: from !== to, from: from + 1, to: to + 1 });
538
+ break;
539
+ }
540
+ default:
541
+ throw new Error(`PDF backend does not support operation: ${operation.op}`);
542
+ }
543
+ }
544
+ await writeFile(path, await document.save({ useObjectStreams: true, addDefaultPage: false }));
545
+ return results;
546
+ }
547
+
548
+ export async function validatePdf(path) {
549
+ const document = await PDFDocument.load(await readFile(path), {
550
+ ignoreEncryption: false,
551
+ updateMetadata: false,
552
+ });
553
+ return {
554
+ ok: true,
555
+ format: 'pdf',
556
+ pages: document.getPageCount(),
557
+ validation: 'pdf-parse',
558
+ };
559
+ }
560
+
561
+ export async function issuesPdf(path, options = {}) {
562
+ const snapshot = await snapshotPdf(path, { ...options, maxChars: options.maxChars || 30_000 });
563
+ const issues = [];
564
+ for (const page of snapshot.pages) {
565
+ if (!page.text || page.text.includes('(no extractable text on this page)')) {
566
+ issues.push({
567
+ severity: 'warning',
568
+ code: 'no_text_layer',
569
+ path: page.path,
570
+ message: 'Page has no extractable text layer; OCR may be required.',
571
+ });
572
+ }
573
+ }
574
+ for (const page of snapshot.likelyScannedPages) {
575
+ issues.push({
576
+ severity: 'warning',
577
+ code: 'ocr_required',
578
+ path: `/page[${page}]`,
579
+ message: 'Page has no text layer. Render at high resolution and use OCR instead of treating it as empty.',
580
+ });
581
+ }
582
+ const widgets = snapshot.fields.flatMap((field) => field.widgets.map((widget) => ({ ...widget, name: field.name, path: field.path })));
583
+ for (let left = 0; left < widgets.length; left += 1) {
584
+ for (let right = left + 1; right < widgets.length; right += 1) {
585
+ if (widgets[left].page === widgets[right].page && rectanglesOverlap(widgets[left], widgets[right])) {
586
+ issues.push({ severity: 'warning', code: 'overlapping_form_fields', path: widgets[left].path, message: `Form fields ${widgets[left].name} and ${widgets[right].name} overlap.` });
587
+ }
588
+ }
589
+ }
590
+ return { ok: true, format: 'pdf', issueCount: issues.length, issues };
591
+ }